diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..fc7a035 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,4 @@ +[build] +# Make sure we always build for local CPU in order to maximize performance: there's rarely if ever a case where +# we want to transfer the binary to a different computer (in which case this would have to be overridden) +rustflags = ["-C", "target-cpu=native"] diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b91fcdb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +assets/**/*.msh filter=lfs diff=lfs merge=lfs -text +assets/**/*.vtk filter=lfs diff=lfs merge=lfs -text +assets/**/*.svg filter=lfs diff=lfs merge=lfs -text +*.blend filter=lfs diff=lfs merge=lfs -text +*.mp4 filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de02d9c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/data +/target +/.idea +.ipynb_checkpoints/ +**/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..14ca5a0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2459 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr 2.4.1", +] + +[[package]] +name = "alga" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f823d037a7ec6ea2197046bafd4ae150e6bc36f9ca347404f46a46823fa84f2" +dependencies = [ + "approx", + "num-complex 0.2.4", + "num-traits", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "approx" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "as-slice" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0" +dependencies = [ + "generic-array 0.12.4", + "generic-array 0.13.3", + "generic-array 0.14.4", + "stable_deref_trait", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bindgen" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c0bb6167449588ff70803f4127f0684f9063097eca5016f37eb52b92c2cf36" +dependencies = [ + "bitflags", + "cexpr", + "cfg-if 0.1.10", + "clang-sys", + "clap", + "env_logger", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + +[[package]] +name = "bit-set" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr 2.4.1", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" + +[[package]] +name = "bytecount" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cargo-platform" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714a157da7991e23d90686b9524b9e12e0407a108647f52e9328f4b3d51ac7f" +dependencies = [ + "cargo-platform", + "semver 0.11.0", + "semver-parser", + "serde", + "serde_json", +] + +[[package]] +name = "cast" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c24dab4283a142afa2fdca129b80ad2c6284e073930f964c3a1293c225ee39a" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "cc" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" + +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom 5.1.2", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "clang-sys" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6837df1d5cba2397b835c8530f51723267e16abbf83892e9e5af4f0e5dd10a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "cmake" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b858541263efe664aead4a5209a4ae5c5d2811167d4ed4ee0944503f8d2089" +dependencies = [ + "cc", +] + +[[package]] +name = "coarse-prof" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97aadcb74f45a014516348b86413b1a7784dd600c5084af1b94c15bb1379f7ff" +dependencies = [ + "floating-duration", + "log", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "criterion" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1604dafd25fba2fe2d5895a9da139f8dc9b319a5fe5354ca137cbbce4e178d10" +dependencies = [ + "atty", + "cast", + "clap", + "criterion-plot", + "csv", + "itertools 0.10.1", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_cbor", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00996de9f2f7559f7f4dc286073197f83e92256a59ed395f9aac01fe717da57" +dependencies = [ + "cast", + "itertools 0.10.1", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if 1.0.0", + "lazy_static", +] + +[[package]] +name = "csv" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +dependencies = [ + "bstr", + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr 2.4.1", +] + +[[package]] +name = "ctor" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "data-buffer" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a050a2db4139046e38b233349fd59ea7cb91c38734da22225784fb79b0418dd" +dependencies = [ + "num-traits", + "reinterpret", +] + +[[package]] +name = "delegate" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bc6b43b1626e75c776f282bcf592b58c6fdd9da069ecf733111a061f6a549a7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dirs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "erased-serde" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3de9ad4541d99dc22b59134e7ff8dc3d6c988c89ecd7324bf10a8362b07a2afa" +dependencies = [ + "serde", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + +[[package]] +name = "fcm_convergence" +version = "0.1.0" +dependencies = [ + "chrono", + "fenris", + "hamilton2", + "mkl-corrode", + "rayon", + "serde", + "serde_json", + "simulation_toolbox", + "structopt", +] + +[[package]] +name = "fenris" +version = "0.1.0" +dependencies = [ + "alga", + "arrayvec", + "criterion", + "delegate", + "hamilton2", + "itertools 0.9.0", + "log", + "lp-bfp", + "matrixcompare", + "mkl-corrode", + "nalgebra", + "nested-vec", + "num 0.2.1", + "numeric_literals", + "ordered-float", + "paradis", + "paste", + "prettytable-rs", + "proptest", + "rayon", + "rstar", + "rustc-hash", + "serde", + "thread_local", + "vtkio", +] + +[[package]] +name = "fern" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9a4820f0ccc8a7afd67c39a0f1a0f4b07ca1725164271a64939d7aeb9af065" +dependencies = [ + "log", +] + +[[package]] +name = "fixedbitset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" + +[[package]] +name = "floating-duration" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2c60b71d9dbcd810a3be879dc9aafac6cec5c50dc2346e245f61f54a61fdf22" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "fs_extra" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f797e67af32588215eaaab8327027ee8e71b9dd0b2b26996aedf20c030fce309" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "ghost" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5bcf1bbeab73aa4cf2fde60a846858dc036163c7c33bec309f8d17de785479" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "global_stash" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "globset" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "gnuplot" +version = "0.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6224c8c5b1d03bf00840caa92ef1db97d379a1efe55db92e76083dffc4058a5" +dependencies = [ + "byteorder", +] + +[[package]] +name = "half" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac5956d4e63858efaec57e0d6c1c2f6a41e1487f830314a324ccd7e2223a7ca0" + +[[package]] +name = "hamilton" +version = "0.1.0" +dependencies = [ + "erased-serde", + "once_cell", + "serde", + "serde_json", +] + +[[package]] +name = "hamilton2" +version = "0.1.0" +dependencies = [ + "alga", + "approx", + "coarse-prof", + "itertools 0.9.0", + "log", + "nalgebra", + "numeric_literals", +] + +[[package]] +name = "hash32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heapless" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634bd4d29cbf24424d0a4bfcbf80c6960129dc24424752a7d1d1390607023422" +dependencies = [ + "as-slice", + "generic-array 0.14.4", + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "ignore" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +dependencies = [ + "crossbeam-utils", + "globset", + "lazy_static", + "log", + "memchr 2.4.1", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg 1.0.1", + "hashbrown", +] + +[[package]] +name = "intel-mkl-src" +version = "0.5.0" +dependencies = [ + "mkl-sys", +] + +[[package]] +name = "inventory" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0f7efb804ec95e33db9ad49e4252f049e37e8b0a4652e3cd61f7999f2eff7f" +dependencies = [ + "ctor", + "ghost", + "inventory-impl", +] + +[[package]] +name = "inventory-impl" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c094e94816723ab936484666968f5b58060492e880f3c8d00489a1e244fa51" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "js-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lapack" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e1a9738b713f1f07481e8f5999ce1e4f7e076938364c69ae00465d0feffa3d" +dependencies = [ + "lapack-sys", + "libc", + "num-complex 0.2.4", +] + +[[package]] +name = "lapack-src" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f80e1175c1e7517c63ec50552bb1d3bcaf1f6d5d689c512adf8c5f11234538e9" +dependencies = [ + "intel-mkl-src", +] + +[[package]] +name = "lapack-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d3a8a9f07310243de6c6226f039f14bce8d2f4c96b5d30ddbcfa31eb4e94ad" +dependencies = [ + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if 1.0.0", + "ryu", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce" + +[[package]] +name = "libloading" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" +dependencies = [ + "cc", + "winapi", +] + +[[package]] +name = "libm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "lp-bfp" +version = "0.1.0" +dependencies = [ + "cmake", + "libc", + "nalgebra", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matrixcompare" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37832ba820e47c93d66b4360198dccb004b43c74abc3ac1ce1fed54e65a80445" +dependencies = [ + "matrixcompare-core", + "num-traits", +] + +[[package]] +name = "matrixcompare-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0bdabb30db18805d5290b3da7ceaccbddba795620b86c02145d688e04900a73" + +[[package]] +name = "matrixmultiply" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916806ba0031cd542105d916a97c8572e1fa6dd79c9c51e7eb43a09ec2dd84c1" +dependencies = [ + "rawpointer", +] + +[[package]] +name = "memchr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "memoffset" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg 1.0.1", +] + +[[package]] +name = "mkl-corrode" +version = "0.1.0" +dependencies = [ + "mkl-sys", +] + +[[package]] +name = "mkl-sys" +version = "0.1.0" +dependencies = [ + "bindgen", +] + +[[package]] +name = "mshio" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01f3aa844a89eb69a2f58cea74055711d77356e20490cdd0a3670117a96a8e2e" +dependencies = [ + "nom 5.1.2", + "num 0.3.1", + "num-derive", + "num-traits", + "thiserror", +] + +[[package]] +name = "nalgebra" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6147c3d50b4f3cdabfe2ecc94a0191fd3d6ad58aefd9664cf396285883486" +dependencies = [ + "approx", + "generic-array 0.13.3", + "matrixmultiply", + "num-complex 0.2.4", + "num-rational 0.2.4", + "num-traits", + "rand 0.7.3", + "rand_distr", + "serde", + "serde_derive", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-lapack" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b6beb98296586f0bb7c97ebc5685e5698289148a1f8c00c5afba69f2d48f820" +dependencies = [ + "lapack", + "lapack-src", + "nalgebra", + "num-complex 0.2.4", + "num-traits", + "simba", +] + +[[package]] +name = "nested-vec" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "nom" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05aec50c70fd288702bcd93284a8444607f3292dbdf2a30de5ea5dcdbe72287b" +dependencies = [ + "memchr 1.0.2", +] + +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "lexical-core", + "memchr 2.4.1", + "version_check", +] + +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint 0.2.6", + "num-complex 0.2.4", + "num-integer", + "num-iter", + "num-rational 0.2.4", + "num-traits", +] + +[[package]] +name = "num" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7a8e9be5e039e2ff869df49155f1c06bd01ade2117ec783e56ab0932b67a8f" +dependencies = [ + "num-bigint 0.3.3", + "num-complex 0.3.1", + "num-integer", + "num-iter", + "num-rational 0.3.2", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg 1.0.1", + "num-traits", + "serde", +] + +[[package]] +name = "num-complex" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg 1.0.1", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg 1.0.1", + "num-bigint 0.2.6", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg 1.0.1", + "num-bigint 0.3.3", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg 1.0.1", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "numeric_literals" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "095aa67b0b9f2081746998f4f17106bdb51d56dc8c211afca5531b92b83bf98a" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "obj" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059c95245738cdc7b40078cdd51a23200252a4c0a0a6dd005136152b3f467a4a" + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "ordered-float" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3305af35278dd29f46fcdd139e0b1fbfae2153f0e5928b39b035542dd31e37b7" +dependencies = [ + "num-traits", +] + +[[package]] +name = "osqp" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e075624701220e1d09157ce82af4dc1592429ce7ee300b404a5d4f27ef4271e" +dependencies = [ + "osqp-sys", +] + +[[package]] +name = "osqp-sys" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7767aa83dba70b46f4fbf9a9662c3b2710db0fc5e74cfa16c2991d049ed17faa" +dependencies = [ + "cc", + "cmake", + "fs_extra", +] + +[[package]] +name = "paradis" +version = "0.1.0" +dependencies = [ + "nested-vec", + "proptest", + "rand 0.7.3", + "rayon", + "serde", +] + +[[package]] +name = "paste" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" +dependencies = [ + "paste-impl", + "proc-macro-hack", +] + +[[package]] +name = "paste-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" +dependencies = [ + "proc-macro-hack", +] + +[[package]] +name = "pdqselect" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec91767ecc0a0bbe558ce8c9da33c068066c57ecc8bb8477ef8c1ad3ef77c27" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "peg" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f76678828272f177ac33b7e2ac2e3e73cc6c1cd1e3e387928aa69562fa51367" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "636d60acf97633e48d266d7415a9355d4389cea327a193f87df395d88cd2b14d" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555b1514d2d99d78150d3c799d4c357a3e2c2a8062cd108e93a06d9057629c5" + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "petgraph" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "plotters" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a3fd9ec30b9749ce28cd91f255d569591cdf937fe280c312143e3c4bad6f2a" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d88417318da0eaf0fdcdb51a0ee6c3bed624333bff8f946733049380be67ac1c" + +[[package]] +name = "plotters-svg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521fa9638fa597e1dc53e9412a4f9cefb01187ee1f7413076f9e6749e2885ba9" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "ply-rs" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbadf9cb4a79d516de4c64806fe64ffbd8161d1ac685d000be789fb628b88963" +dependencies = [ + "byteorder", + "linked-hash-map", + "peg", + "skeptic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ca011bd0129ff4ae15cd04c4eef202cadf6c51c21e47aba319b4e0501db741" + +[[package]] +name = "prettytable-rs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd04b170004fa2daccf418a7f8253aaf033c27760b5f225889024cf66d7ac2e" +dependencies = [ + "atty", + "csv", + "encode_unicode", + "lazy_static", + "term", + "unicode-width", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "proptest" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c477819b845fe023d33583ebf10c9f62518c8d79a0960ba5c36d6ac8a55a5b" +dependencies = [ + "bit-set", + "bitflags", + "byteorder", + "lazy_static", + "num-traits", + "quick-error", + "rand 0.6.5", + "rand_chacha 0.1.1", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", +] + +[[package]] +name = "pulldown-cmark" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" +dependencies = [ + "bitflags", + "memchr 2.4.1", + "unicase", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.7", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.3", +] + +[[package]] +name = "rand_distr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96977acbdd3a6576fb1d27391900035bf3863d4a16422973a409b488cf29ffb2" +dependencies = [ + "rand 0.7.3", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +dependencies = [ + "autocfg 1.0.1", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom 0.1.16", + "redox_syscall 0.1.57", + "rust-argon2", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr 2.4.1", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "reinterpret" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "318d0078beaf244bfa5dcd06174634339e41f3d21650ffcf70f4194d8852921a" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rstar" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d535e658ada8c1987a113e5261f8b907f721b2854d666e72820671481b7ee125" +dependencies = [ + "heapless", + "num-traits", + "pdqselect", + "serde", + "smallvec", +] + +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver 1.0.4", +] + +[[package]] +name = "rusty-fork" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dd93264e10c577503e926bd1430193eeb5d21b059148910082245309b424fae" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scene_runner" +version = "0.1.0" +dependencies = [ + "chrono", + "coarse-prof", + "fenris", + "fern", + "global_stash", + "gnuplot", + "hamilton", + "hostname", + "ignore", + "itertools 0.9.0", + "log", + "mkl-corrode", + "nalgebra-lapack", + "numeric_literals", + "once_cell", + "petgraph", + "ply-rs", + "rand 0.7.3", + "rayon", + "serde", + "simulation_toolbox", + "statrs", + "structopt", + "typetag", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", + "serde", +] + +[[package]] +name = "semver" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" + +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + +[[package]] +name = "simba" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb931b1367faadea6b1ab1c306a860ec17aaa5fa39f367d0c744e69d971a1fb2" +dependencies = [ + "approx", + "num-complex 0.2.4", + "num-traits", + "paste", +] + +[[package]] +name = "simulation_toolbox" +version = "0.1.0" +dependencies = [ + "coarse-prof", + "fenris", + "global_stash", + "hamilton", + "hamilton2", + "intel-mkl-src", + "itertools 0.9.0", + "lapack-src", + "log", + "mkl-corrode", + "mshio", + "nalgebra-lapack", + "num 0.2.1", + "numeric_literals", + "obj", + "osqp", + "paradis", + "ply-rs", + "rayon", + "rstar", + "serde", + "serde_json", + "typetag", +] + +[[package]] +name = "skeptic" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "188b810342d98f23f0bb875045299f34187b559370b041eb11520c905370a888" +dependencies = [ + "bytecount", + "cargo_metadata", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", +] + +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "statrs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce16f6de653e88beca7bd13780d08e09d4489dbca1f9210e041bc4852481382" +dependencies = [ + "rand 0.7.3", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9788f4202aa75c240ecc9c15c65185e6a39ccdeb0fd5d008b98825464c87c" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "rand 0.8.4", + "redox_syscall 0.2.10", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "term" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" +dependencies = [ + "byteorder", + "dirs", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "typenum" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" + +[[package]] +name = "typetag" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "422619e1a7299befb977a1f6d8932c499f6151dbcafae715193570860cae8f07" +dependencies = [ + "erased-serde", + "inventory", + "lazy_static", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504f9626fe6cc1c376227864781996668e15c1ff251d222f63ef17f310bf1fec" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "vtkio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e16370105ad4653f37b21f7bcb40d1c0c35d853e4558c5da6ed091f587453c4" +dependencies = [ + "byteorder", + "data-buffer", + "nom 3.2.1", + "num-derive", + "num-traits", +] + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" + +[[package]] +name = "web-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b31e9d6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,37 @@ +[workspace] + +members = [ + "simulation_toolbox", + "scene_runner", + "fenris", + "hamilton", + "lp-bfp", + "nested-vec", + "paradis", + "intel-mkl-src-patched", + "hamilton2", + "global_stash", + "fcm_convergence", +] + +[profile.release] +# Enable debug information if it becomes necessary to debug a release build. Otherwise, disable it, +# as it contributes significantly to compile times. +#debug = true +incremental = true + +[profile.bench] +debug=true + +# Patch intel-mkl-src to use our own version, which uses `mkl-sys` under the hood. +# This lets us use both mkl-corrode and MKL LAPACK bindings through nalgebra in the same code base +[patch.crates-io] +intel-mkl-src = { path = "intel-mkl-src-patched" } + +# Override mkl-sys and mkl-corrode dependencies with local code in order to ensure +# that we don't depend on external git repositories +[patch."https://github.com/Andlon/mkl-sys.git"] +mkl-sys = { path = "extern/mkl-sys" } + +[patch."https://github.com/Andlon/mkl-corrode.git"] +mkl-corrode = { path = "extern/mkl-corrode" } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d7eb1ab --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2021 Andreas Longva, Fabian Löschner, Jan Bender + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f1a8d6a --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# Higher-order Finite Elements for Embedded Simulation + +This repository contains the source code used to produce the results for our paper: + +[Longva, A., Löschner, F., Kugelstadt, T., Fernández-Fernández, J.A. and Bender, J., 2020. +*Higher-order finite elements for embedded simulation*. +ACM Transactions on Graphics (TOG), 39(6), pp.1-14.](https://dl.acm.org/doi/10.1145/3414685.3417853) + +Please also see the [entry on our website](https://animation.rwth-aachen.de/publication/0572/) +for access to the paper, the supplemental document, video and more. + +## Important information + +The code provided here is almost entirely unchanged from the code used to generate the results +demonstrated in the paper. After publication, we have discovered a couple of mistakes that we detail +in our [errata](errata.pdf). The code has deliberately **not** been updated to account for these mistakes. + +The code is provided only for the sake of giving researchers the ability to reproduce our results. +It is not intended to serve as a base for building your own project on top of. The published code is the +result of violently ripping out only the relevant parts from a yet larger code base used for multiple papers. +Having grown organically it is generally overly complex and unwieldy. We are actively working on redesigning, +cleaning up, documenting and packaging some of the core functionality as reusable libraries. +The FEM portion of the code is currently in the process of being redesigned and repackaged as the standalone +[Fenris FEM library](https://github.com/InteractiveComputerGraphics/fenris). + +If you have issues, questions or comments, please feel free to contact Andreas (first author) directly, +or write up an issue here on the GitHub tracker. + +## Build instructions + +The code is written in the [Rust](https://www.rust-lang.org) programming language. + +Although Rust comes with an excellent automated build system, our code relies on a couple of libraries +that need to be installed separately: + +- Intel MKL (tested with Intel MKL 2020) +- libclang (required for automatically generating bindings to MKL) +- Google OR Tools (required for Simplex solver, tested with version 7.7) + +The code should generally work on any of the common platforms (Linux, Windows, Mac), +but it has been tested most extensively on Linux (and not at all on Mac). + +In order to make this repository stand-alone, we have bundled the libraries +[mkl-sys](https://github.com/Andlon/mkl-sys) and [mkl-corrode](https://github.com/Andlon/mkl-sys), +that were not available on the central Rust dependency repository `crates.io` into the `extern/` folder of this repositroy. + +Once you have installed Rust and the required dependencies (see below for more details), you can build the project +by invoking `cargo` (Rust package manager) from the root of this repository: + +``` +cargo build --release +``` + +This will download and build the required Rust dependencies (of which there are many). + +## Running experiments + +In order to run the experiments, you will need to download the binary archive containing the asset files (meshes etc.) +made available through GitHub releases. Unzip the archive into the root directory, which should give +you an `assets/` folder. + +The easiest way to run experiments is to run the provided Python wrapper files in the `experiments` folder. +For example, to run the "Cylinder shell" example (referred to as "hollow cylinder" in our paper), invoke the +corresponding Python script from the project root directory: +``` +python experiments/cylinder_shell/run_experiments.py +``` +This will (re-)compile the code and run the experiment for a number of configurations +(mesh resolution, element order, FEM vs. FCM etc.). You might want to check out the Python file +and make some adjustments. This process will produce a large amount of output in the +experiment-specific data directory (see Python script for output directory), including +log files, VTK files for analysis in Paraview and PLY surface meshes that can be used for rendering. + +**NB!** We are sadly not permitted to directly redistribute the mesh data for the "Hollow Ball" scene +due to licensing restrictions. See `assets/meshes/hollow_ball/README` for more information. + +Additionally, instead of running the Python scripts (which are not available for *every* experiment), +you can explore the scenes provided in the binary itself by running: + +``` +# Produces the help text for the scene runner binary +cargo run --release -- --help + +# Produces a list of available scenes +cargo run --release -- --list-scenes + +# Run a specific scene +cargo run --release -- --scene bicycle_fem_fine +``` + +## Installing external dependencies + +### Intel MKL + +Recently, Intel rebranded its MKL library as +[oneMKL](https://www.intel.com/content/www/us/en/developer/tools/oneapi/onemkl.html). +We used Intel MKL 2020 for our experiments, but we believe it should also work with the more recent oneMKL versions. + +Install the library as described by the (one)MKL documentation. Our code look for the `MKLROOT` +environment variable. This environment variable gets automatically set if you run the +configuration script provided alongside MKL. For example, on Linux: + +``` +source /path/to/intel/mkl/bin/mklvars.sh intel64 +``` + +### LLVM / libclang + +Follow the instructions provided for the +[bindgen project](https://rust-lang.github.io/rust-bindgen/requirements.html), +which is used by the `mkl-sys` library to automatically generate usable Rust bindings +from the MKL C headers. + +### Google OR Tools + +Download the [binary OR-Tools distribution](https://developers.google.com/optimization/install/cpp) and set the +the `ORTOOLS_ROOT` environment variable to the root directory of the OR Tools installation. +In addition, you need to add the OR Tools libraries to the linker library search paths. +On Linux, this might look like: + +``` +export ORTOOLS_ROOT=/path/to/or-tools-7.7.810 +export LD_LIBRARY_PATH=$ORTOOLS_ROOT/lib:$LD_LIBRARY_PATH +``` + +## License + +The Rust source code provided in this repository (with the exception of code in `extern/`, +which is separately licensed) is licensed under the MIT license. See `LICENSE` for details. + +Dependencies are licensed under their own respective licenses. Please see `assets/README` for +information about license terms for the meshes we use. \ No newline at end of file diff --git a/errata.pdf b/errata.pdf new file mode 100644 index 0000000..ab85de7 Binary files /dev/null and b/errata.pdf differ diff --git a/errata/tex/.gitignore b/errata/tex/.gitignore new file mode 100644 index 0000000..09053be --- /dev/null +++ b/errata/tex/.gitignore @@ -0,0 +1,254 @@ +errata.pdf + + +#### TeX ignores below + +## Core latex/pdflatex auxiliary files: +*.aux +*.lof +*.log +*.lot +*.fls +*.out +*.toc +*.fmt +*.fot +*.cb +*.cb2 +.*.lb + +## Intermediate documents: +*.dvi +*.xdv +*-converted-to.* +# these rules might exclude image files for figures etc. +# *.ps +# *.eps +# *.pdf + +## Generated if empty string is given at "Please type another file name for output:" +.pdf + +## Bibliography auxiliary files (bibtex/biblatex/biber): +*.bbl +*.bcf +*.blg +*-blx.aux +*-blx.bib +*.run.xml + +## Build tool auxiliary files: +*.fdb_latexmk +*.synctex +*.synctex(busy) +*.synctex.gz +*.synctex.gz(busy) +*.pdfsync + +## Build tool directories for auxiliary files +# latexrun +latex.out/ + +## Auxiliary and intermediate files from other packages: +# algorithms +*.alg +*.loa + +# achemso +acs-*.bib + +# amsthm +*.thm + +# beamer +*.nav +*.pre +*.snm +*.vrb + +# changes +*.soc + +# cprotect +*.cpt + +# elsarticle (documentclass of Elsevier journals) +*.spl + +# endnotes +*.ent + +# fixme +*.lox + +# feynmf/feynmp +*.mf +*.mp +*.t[1-9] +*.t[1-9][0-9] +*.tfm + +#(r)(e)ledmac/(r)(e)ledpar +*.end +*.?end +*.[1-9] +*.[1-9][0-9] +*.[1-9][0-9][0-9] +*.[1-9]R +*.[1-9][0-9]R +*.[1-9][0-9][0-9]R +*.eledsec[1-9] +*.eledsec[1-9]R +*.eledsec[1-9][0-9] +*.eledsec[1-9][0-9]R +*.eledsec[1-9][0-9][0-9] +*.eledsec[1-9][0-9][0-9]R + +# glossaries +*.acn +*.acr +*.glg +*.glo +*.gls +*.glsdefs + +# gnuplottex +*-gnuplottex-* + +# gregoriotex +*.gaux +*.gtex + +# htlatex +*.4ct +*.4tc +*.idv +*.lg +*.trc +*.xref + +# hyperref +*.brf + +# knitr +*-concordance.tex +# TODO Comment the next line if you want to keep your tikz graphics files +*.tikz +*-tikzDictionary + +# listings +*.lol + +# makeidx +*.idx +*.ilg +*.ind +*.ist + +# minitoc +*.maf +*.mlf +*.mlt +*.mtc[0-9]* +*.slf[0-9]* +*.slt[0-9]* +*.stc[0-9]* + +# minted +_minted* +*.pyg + +# morewrites +*.mw + +# nomencl +*.nlg +*.nlo +*.nls + +# pax +*.pax + +# pdfpcnotes +*.pdfpc + +# sagetex +*.sagetex.sage +*.sagetex.py +*.sagetex.scmd + +# scrwfile +*.wrt + +# sympy +*.sout +*.sympy +sympy-plots-for-*.tex/ + +# pdfcomment +*.upa +*.upb + +# pythontex +*.pytxcode +pythontex-files-*/ + +# tcolorbox +*.listing + +# thmtools +*.loe + +# TikZ & PGF +*.dpth +*.md5 +*.auxlock + +# todonotes +*.tdo + +# easy-todo +*.lod + +# xmpincl +*.xmpi + +# xindy +*.xdy + +# xypic precompiled matrices +*.xyc + +# endfloat +*.ttt +*.fff + +# Latexian +TSWLatexianTemp* + +## Editors: +# WinEdt +*.bak +*.sav + +# Texpad +.texpadtmp + +# LyX +*.lyx~ + +# Kile +*.backup + +# KBibTeX +*~[0-9]* + +# auto folder when using emacs and auctex +./auto/* +*.el + +# expex forward references with \gathertags +*-tags.tex + +# standalone packages +*.sta diff --git a/errata/tex/errata.tex b/errata/tex/errata.tex new file mode 100644 index 0000000..e476b4d --- /dev/null +++ b/errata/tex/errata.tex @@ -0,0 +1,92 @@ +\documentclass{scrartcl} + +\usepackage{amsmath} +\usepackage{hyperref} +\usepackage{siunitx} + +\title{Higher-order finite elements for embedded simulation} +\subtitle{Errata} +\date{October 2021} +\author{Andreas Longva, RWTH Aachen University} + +\begin{document} +\maketitle + +This document details some errors discovered in our source code after the publication of our paper. + +The code used to produce most of the results for our paper is available at the following URL: + +\begin{center} +\url{https://github.com/InteractiveComputerGraphics/higher_order_embedded_fem} +\end{center} + +\section*{Material parameters} +While rewriting and transfering some code to a new project, we discovered that our utility function for converting material parameters from Young's modulus and Poisson's ratio to corresponding LamĂ© parameters was off by a factor 4. + +In short, when converting to the LamĂ© parameters $\lambda$ and $\mu$, our code used an incorrect formula for computing $\lambda$, whereas the formula used for $\mu$ was correct. The formulas we had implemented were: +\begin{align*} +\mu = \frac{E}{2 (1 + \nu)}, \qquad +\lambda^\text{bad} = \frac{1}{2} \cdot \frac{\mu \nu}{1 - 2 \nu}. \\ +\end{align*} +The correct formula for $\lambda$ is +\begin{align*} +\lambda = 2 \cdot \frac{\mu \nu}{1 - 2 \nu} = 4 \lambda^\text{bad}. +\end{align*} + +As a result, the parameters we give in the paper are not the \emph{true} parameters that were used for the simulation. With the incorrect formulas we can compute the LamĂ© parameters that were used for the simulation, and use the \emph{correct} conversion formulas the other way to compute corresponding \emph{effective} values for the Young's modulus and Poisson's ratio that would lead to the same LamĂ© parameters, and therefore the same simulation results. We obtain: +\begin{align*} +\nu^\text{eff} = \frac{\nu^\text{paper}}{4 - 6 \, \nu^\text{paper}}, +\qquad +E^\text{eff} = E^\text{paper} \frac{1 + \nu^\text{eff}}{1 + \nu^\text{paper}}. +\end{align*} + +The following table gives the original material parameters (as presented in the paper) and the corresponding effective values (up to 4 significant digits) for the experiments in which these material parameters were provided. + +\begin{center} +\begin{tabular}{l | c | c} +\textbf{Experiment} & \textbf{Paper parameters} & \textbf{Effective parameters} \\ +\hline +Quadrature verification (Section 5.4) +& +\begin{tabular}{l} +$E = \SI{3e6}{\pascal}$ \\ $\nu = 0.4$ +\end{tabular} +& \begin{tabular}{l} +$E = \SI{2.679e6}{\pascal}$ \\ $\nu = 0.25$ +\end{tabular} +\\ +\hline +Twisting cylinder (Section 6.2) +& +\begin{tabular}{l} +$E = \SI{5e6}{\pascal}$ \\ $\nu = 0.48$ +\end{tabular} +& \begin{tabular}{l} +$E = \SI{4.826e6}{\pascal}$ \\ $\nu = 0.4286$ +\end{tabular} +\\ +\hline +Armadillo slingshot (Section 6.5) +& +\begin{tabular}{l} +$E = \SI{5e5}{\pascal}$ \\ $\nu = 0.4$ +\end{tabular} +& \begin{tabular}{l} +$E = \SI{4.464e5}{\pascal}$ \\ $\nu = 0.25$ +\end{tabular} +\\ +\hline +\end{tabular} +\end{center} + +We see that the resulting effective parameters are (unfortunately) noticably different from the intended parameters. However, these parameter choices were more or less arbitrary to begin with, so it seems fair to say that it does not significantly change any of the conclusions made in the paper. + + + +\section*{Twisting cylinder boundary conditions} + +The twisting cylinder (Section 6.2) is reported in the paper to be 16 meters long. Recently, upon reviewing the code, we realized that the Dirichlet boundary conditions at the end were enforced for nodes with $y$-coordinate $|y| \geq 6.99$ instead of $|y| \geq 7.99$. Therefore the motion of the last meter on each side of the cylinder is prescribed. The experiment therefore effectively simulates a 14 meter long cylinder as opposed to the 16 meters described. + + +\section*{} +\end{document} \ No newline at end of file diff --git a/experiments/armadillo_slingshot/run_experiments.py b/experiments/armadillo_slingshot/run_experiments.py new file mode 100644 index 0000000..4cb19d0 --- /dev/null +++ b/experiments/armadillo_slingshot/run_experiments.py @@ -0,0 +1,51 @@ +import os +import subprocess + +# Values are intended for Suttner +# You might want to set this to the number of *physical* cores on your computer +# os.environ['RAYON_NUM_THREADS'] = "28" +# os.environ['OMP_NUM_THREADS'] = "28" +os.environ['RUSTFLAGS'] = "-C target-cpu=native" + +data_dir = "data_armadillo_slingshot" + +# We assume that the script is called from the root project directory +args = [ + "cargo", + "run", + "--", + "--list-scenes" +] + +subprocess.run(["cargo build --release"], shell=True) + +scenes = [ + "armadillo_slingshot_embedded_tet4_500", + "armadillo_slingshot_embedded_tet4_1500", + "armadillo_slingshot_embedded_tet4_3000", + "armadillo_slingshot_embedded_tet10_500", + "armadillo_slingshot_embedded_tet10_1000", + "armadillo_slingshot_embedded_tet10_1500", + "armadillo_slingshot_embedded_tet10_3000", + "armadillo_slingshot_fem_tet4_500", + "armadillo_slingshot_fem_tet4_1500", + "armadillo_slingshot_fem_tet4_3000", + "armadillo_slingshot_fem_tet10_500", + "armadillo_slingshot_fem_tet10_1000", + "armadillo_slingshot_fem_tet10_3000", + + # Save the more expensive sims for last + "armadillo_slingshot_fem_tet4_5000", + "armadillo_slingshot_embedded_tet4_5000", + "armadillo_slingshot_fem_tet10_5000", + "armadillo_slingshot_embedded_tet10_5000", + "armadillo_slingshot_fem_tet4_full", +] + +for scene in scenes: + print("\n\n\nRunning scene {}\n\n\n".format(scene)) + cmd = "target/release/dynamic_runner --scene {} --print-timings-output --output-dir {} --output-ply" \ + " --output-fps 100 --dump-stash"\ + .format(scene, data_dir) + subprocess.run([cmd], shell=True) + diff --git a/experiments/convergence_rate/run_experiments.py b/experiments/convergence_rate/run_experiments.py new file mode 100644 index 0000000..c5e3830 --- /dev/null +++ b/experiments/convergence_rate/run_experiments.py @@ -0,0 +1,14 @@ +import os +import subprocess + +# Values are intended for Suttner +# You might want to set this to the number of *physical* cores on your computer +# os.environ['RAYON_NUM_THREADS'] = "28" +# os.environ['OMP_NUM_THREADS'] = "28" +os.environ['RUSTFLAGS'] = "-C target-cpu=native" + +data_dir = "data_cylinder_shell" + +# We assume that the script is called from the root project directory +command = "cargo run --release --bin fcm_convergence -- --resolutions 1 2 4 8 16 24 32 --reference-mesh hemisphere_50_uniform_refined2.msh" +subprocess.run([command], shell=True).check_returncode() diff --git a/experiments/cylinder_shell/run_experiments.py b/experiments/cylinder_shell/run_experiments.py new file mode 100644 index 0000000..4d75de5 --- /dev/null +++ b/experiments/cylinder_shell/run_experiments.py @@ -0,0 +1,71 @@ +import os +import subprocess + +# Values are intended for Suttner +# You might want to set this to the number of *physical* cores on your computer +# os.environ['RAYON_NUM_THREADS'] = "28" +# os.environ['OMP_NUM_THREADS'] = "28" +os.environ['RUSTFLAGS'] = "-C target-cpu=native" + +data_dir = "data_cylinder_shell" + +# We assume that the script is called from the root project directory +args = [ + "cargo", + "run", + "--", + "--list-scenes" +] + +subprocess.run(["cargo build --release"], shell=True).check_returncode() + +scenes = [ + # "cylinder_shell_embedded_hex20_res1", + # "cylinder_shell_embedded_hex20_res2", + # "cylinder_shell_embedded_hex20_res3", + # "cylinder_shell_embedded_hex20_res4", + # "cylinder_shell_embedded_hex20_res5", + # "cylinder_shell_embedded_hex20_res6_strength3", + # "cylinder_shell_embedded_hex20_res6_strength5", + # "cylinder_shell_embedded_hex20_res6_strength5_no_simp", + "cylinder_shell_embedded_hex20_res7_strength3", + "cylinder_shell_embedded_hex20_res7_strength5", + # "cylinder_shell_embedded_hex20_res8", + # "cylinder_shell_embedded_hex20_res10", + # "cylinder_shell_embedded_hex8_res2", + # "cylinder_shell_embedded_hex8_res3", + # "cylinder_shell_embedded_hex8_res5", + # "cylinder_shell_embedded_hex8_res10", + # "cylinder_shell_embedded_hex8_res14", + # "cylinder_shell_embedded_hex8_res16", + # "cylinder_shell_embedded_hex8_res18", + # "cylinder_shell_embedded_hex8_res20", + # "cylinder_shell_embedded_hex8_res22", + # "cylinder_shell_embedded_hex8_res24", + # "cylinder_shell_embedded_hex8_res26", + "cylinder_shell_embedded_hex8_res28", + "cylinder_shell_embedded_hex8_res29", + "cylinder_shell_embedded_hex8_res30", + # "cylinder_shell_embedded_hex8_res32", + # "cylinder_shell_fem_tet4_5k", + # "cylinder_shell_fem_tet4_10k", + # "cylinder_shell_fem_tet4_20k", + # "cylinder_shell_fem_tet4_40k", + "cylinder_shell_fem_tet10_5k_strength2", + "cylinder_shell_fem_tet10_5k_strength3", + "cylinder_shell_fem_tet10_5k_strength5", + # "cylinder_shell_fem_tet10_10k", + # # Run these denser meshes last + # "cylinder_shell_fem_tet4_80k", + "cylinder_shell_fem_tet10_20k", + "cylinder_shell_fem_tet4_160k", + "cylinder_shell_embedded_hex20_res7_strength5_no_simp", + # "cylinder_shell_fem_tet4_320k", +] + +for scene in scenes: + print("\n\n\nRunning scene {}\n\n\n".format(scene)) + cmd = "target/release/dynamic_runner --scene {} --print-timings-output --output-dir {} --output-ply " \ + "--dump-stash"\ + .format(scene, data_dir) + subprocess.run([cmd], shell=True) diff --git a/experiments/hollow_ball/run_experiments.py b/experiments/hollow_ball/run_experiments.py new file mode 100644 index 0000000..a980e80 --- /dev/null +++ b/experiments/hollow_ball/run_experiments.py @@ -0,0 +1,44 @@ +import os +import subprocess + +# Values are intended for Suttner +# You might want to set this to the number of *physical* cores on your computer +# os.environ['RAYON_NUM_THREADS'] = "28" +# os.environ['OMP_NUM_THREADS'] = "28" +os.environ['RUSTFLAGS'] = "-C target-cpu=native" + +data_dir = "data_hollow_ball" + +# We assume that the script is called from the root project directory +args = [ + "cargo", + "run", + "--", + "--list-scenes" +] + +subprocess.run(["cargo build --release"], shell=True) + +scenes = [ + "hollow_ball_embedded_tet4_coarse", + "hollow_ball_embedded_tet10_coarse", + + "hollow_ball_embedded_tet4_medium", + "hollow_ball_embedded_tet10_medium", + "hollow_ball_fem_tet4_medium", + + "hollow_ball_fem_tet4_fine", + "hollow_ball_fem_tet10_medium", + "hollow_ball_fem_tet4_coarse", + "hollow_ball_fem_tet10_coarse", + + "hollow_ball_fem_tet10_fine", +] + +for scene in scenes: + print("\n\n\nRunning scene {}\n\n\n".format(scene)) + cmd = "target/release/dynamic_runner --scene {} --print-timings-output --output-dir {} --output-ply " \ + "--dump-stash --output-fps 100"\ + .format(scene, data_dir) + subprocess.run([cmd], shell=True) + diff --git a/extern/mkl-corrode/.github/workflows/build_and_test.yml b/extern/mkl-corrode/.github/workflows/build_and_test.yml new file mode 100644 index 0000000..01939c4 --- /dev/null +++ b/extern/mkl-corrode/.github/workflows/build_and_test.yml @@ -0,0 +1,280 @@ +name: Build and run tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + CARGO_TERM_COLOR: always + +jobs: + + # --- + # Linux jobs + # --- + + build_ubuntu_mkl2019: + + name: Test on Ubuntu (MKL 2019.5) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install MKL + run: | + wget https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS-2019.PUB + sudo apt-key add GPG-PUB-KEY-INTEL-SW-PRODUCTS-2019.PUB + sudo sh -c 'echo deb https://apt.repos.intel.com/mkl all main > /etc/apt/sources.list.d/intel-mkl.list' + sudo apt update + sudo apt install intel-mkl-64bit-2019.5-075 + + - name: Update Rust + run: rustup update + + - name: Build + run: | + source /opt/intel/mkl/bin/mklvars.sh intel64 + cargo build --verbose + - name: Run tests (--features "dss") + run: | + source /opt/intel/mkl/bin/mklvars.sh intel64 + cargo test --verbose + - name: Build (--release) + run: | + source /opt/intel/mkl/bin/mklvars.sh intel64 + cargo build --release --verbose + - name: Run tests (--release) + run: | + source /opt/intel/mkl/bin/mklvars.sh intel64 + cargo test --release --verbose + + + build_ubuntu_mkl2020: + + name: Test on Ubuntu (MKL 2020.1) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install MKL + run: | + wget https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS-2019.PUB + sudo apt-key add GPG-PUB-KEY-INTEL-SW-PRODUCTS-2019.PUB + sudo sh -c 'echo deb https://apt.repos.intel.com/mkl all main > /etc/apt/sources.list.d/intel-mkl.list' + sudo apt update + sudo apt install intel-mkl-64bit-2020.1-102 + + - name: Update Rust + run: rustup update + + - name: Build + run: | + source /opt/intel/mkl/bin/mklvars.sh intel64 + cargo build --verbose + - name: Run tests (--features "dss") + run: | + source /opt/intel/mkl/bin/mklvars.sh intel64 + cargo test --verbose + - name: Build (--release) + run: | + source /opt/intel/mkl/bin/mklvars.sh intel64 + cargo build --release --verbose + - name: Run tests (--release) + run: | + source /opt/intel/mkl/bin/mklvars.sh intel64 + cargo test --release --verbose + + # --- + # Windows jobs + # --- + + build_windows_mkl2019: + + name: Test on Windows (MKL 2019.5) + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + + # Caching of MKL and clang does not work due to cache size limitations of Github + + # - name: Cache MKL installation + # id: cache-mkl-installation + # uses: actions/cache@v1 + # with: + # path: mkl + # key: ${{ runner.os }}-mkl-installed-w_mkl_2019.5.281 + - name: Download MKL installer + #if: steps.cache-mkl-installation.outputs.cache-hit != 'true' + run: (New-Object System.Net.WebClient).DownloadFile("http://registrationcenter-download.intel.com/akdlm/irc_nas/tec/15806/w_mkl_2019.5.281.exe", "$(($pwd).path)\w_mkl_2019.5.281.exe") + shell: pwsh + - name: Extract MKL installer files + #if: steps.cache-mkl-installation.outputs.cache-hit != 'true' + run: Start-Process .\w_mkl_2019.5.281.exe -ArgumentList @("--silent", "--extract-folder", "$(($pwd).path)\mkl-installer", "--extract-only") -Wait + shell: pwsh + - name: Install MKL + #if: steps.cache-mkl-installation.outputs.cache-hit != 'true' + run: Start-Process .\mkl-installer\install.exe -ArgumentList @("install", "--output=$(($pwd).path)\log.txt", "--eula=accept", "--installdir=$(($pwd).path)\mkl") -Wait + shell: pwsh + # - name: Show log + # if: steps.cache-mkl-installation.outputs.cache-hit != 'true' + # run: gc "$(($pwd).path)\log.txt" + # shell: pwsh + # - name: Find "mklvars.bat" + # run: ls -r -ea silentlycontinue -fo -inc "mklvars.bat" | % { $_.fullname } + # shell: pwsh + # - name: Run mklvars.bat + # run: .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + # shell: pwsh + + # - name: Cache Clang installation + # id: cache-clang-installation + # uses: actions/cache@v1 + # with: + # path: clang + # key: ${{ runner.os }}-clang-installed-LLVM-9.0.0-win64 + - name: Download Clang + #if: steps.cache-clang-installation.outputs.cache-hit != 'true' + run: (New-Object System.Net.WebClient).DownloadFile("https://releases.llvm.org/9.0.0/LLVM-9.0.0-win64.exe", "$(($pwd).path)\LLVM-9.0.0-win64.exe") + shell: pwsh + - name: Install Clang + #if: steps.cache-clang-installation.outputs.cache-hit != 'true' + run: Start-Process .\LLVM-9.0.0-win64.exe -ArgumentList @("/S", "/NCRC", "/D=$(($pwd).path)\clang") -Wait + shell: pwsh + # - name: Test Clang version + # run: .\clang\bin\clang.exe --version + # shell: pwsh + # - name: Find "clang.dll" + # run: ls -r -ea silentlycontinue -fo -inc "libclang.dll" | % { $_.fullname } + # shell: pwsh + + # - name: Download rustup-init + # run: (New-Object System.Net.WebClient).DownloadFile("https://win.rustup.rs/", "$(($pwd).path)\rustup-init.exe") + # shell: pwsh + # - name: Install Rust + # run: .\rustup-init.exe -y --quiet + # shell: pwsh + # - name: Test Rust version + # run: $env:Path = "$env:USERPROFILE\.cargo\bin;$env:Path"; rustc.exe --version + # shell: pwsh + + - name: Update Rust + run: rustup update + shell: pwsh + + # The following steps have to be run in CMD in order to get the environment variables from the .bat scripts + + - name: Build + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + call .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + set LIBCLANG_PATH=%cd%\clang\bin + cargo build --verbose + shell: cmd + - name: Run tests + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + call .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + set LIBCLANG_PATH=%cd%\clang\bin + cargo test --verbose + shell: cmd + + - name: Build (--release) + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + call .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + set LIBCLANG_PATH=%cd%\clang\bin + cargo build --release --verbose + shell: cmd + - name: Run tests (--release) + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + call .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + set LIBCLANG_PATH=%cd%\clang\bin + cargo test --release --verbose + shell: cmd + + + build_windows_mkl2020: + + name: Test on Windows (MKL 2020.1) + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + + # Caching of MKL and clang does not work due to cache size limitations of Github + + # - name: Cache MKL installation + # id: cache-mkl-installation + # uses: actions/cache@v1 + # with: + # path: mkl + # key: ${{ runner.os }}-mkl-installed-w_mkl_2020.0.166 + - name: Download MKL installer + #if: steps.cache-mkl-installation.outputs.cache-hit != 'true' + run: (New-Object System.Net.WebClient).DownloadFile("http://registrationcenter-download.intel.com/akdlm/irc_nas/tec/16543/w_mkl_2020.1.216.exe", "$(($pwd).path)\w_mkl_2020.1.216.exe") + shell: pwsh + - name: Extract MKL installer files + #if: steps.cache-mkl-installation.outputs.cache-hit != 'true' + run: Start-Process .\w_mkl_2020.1.216.exe -ArgumentList @("--silent", "--extract-folder", "$(($pwd).path)\mkl-installer", "--extract-only") -Wait + shell: pwsh + - name: Install MKL + #if: steps.cache-mkl-installation.outputs.cache-hit != 'true' + run: Start-Process .\mkl-installer\install.exe -ArgumentList @("install", "--output=$(($pwd).path)\log.txt", "--eula=accept", "--installdir=$(($pwd).path)\mkl") -Wait + shell: pwsh + + # - name: Cache Clang installation + # id: cache-clang-installation + # uses: actions/cache@v1 + # with: + # path: clang + # key: ${{ runner.os }}-clang-installed-LLVM-9.0.0-win64 + - name: Download Clang + #if: steps.cache-clang-installation.outputs.cache-hit != 'true' + run: (New-Object System.Net.WebClient).DownloadFile("https://releases.llvm.org/9.0.0/LLVM-9.0.0-win64.exe", "$(($pwd).path)\LLVM-9.0.0-win64.exe") + shell: pwsh + - name: Install Clang + #if: steps.cache-clang-installation.outputs.cache-hit != 'true' + run: Start-Process .\LLVM-9.0.0-win64.exe -ArgumentList @("/S", "/NCRC", "/D=$(($pwd).path)\clang") -Wait + shell: pwsh + + - name: Update Rust + run: rustup update + shell: pwsh + + # The following steps have to be run in CMD in order to get the environment variables from the .bat scripts + + - name: Build + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + call .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + set LIBCLANG_PATH=%cd%\clang\bin + cargo build --verbose + shell: cmd + - name: Run tests + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + call .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + set LIBCLANG_PATH=%cd%\clang\bin + cargo test --verbose + shell: cmd + + - name: Build (--release) + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + call .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + set LIBCLANG_PATH=%cd%\clang\bin + cargo build --release --verbose + shell: cmd + - name: Run tests (--release) + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + call .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + set LIBCLANG_PATH=%cd%\clang\bin + cargo test --release --verbose + shell: cmd diff --git a/extern/mkl-corrode/.gitignore b/extern/mkl-corrode/.gitignore new file mode 100644 index 0000000..6936990 --- /dev/null +++ b/extern/mkl-corrode/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/extern/mkl-corrode/Cargo.toml b/extern/mkl-corrode/Cargo.toml new file mode 100644 index 0000000..42f48db --- /dev/null +++ b/extern/mkl-corrode/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "mkl-corrode" +version = "0.1.0" +authors = ["Andreas Longva "] +edition = "2018" + +[features] +ilp64 = [ "mkl-sys/ilp64" ] +openmp = [ "mkl-sys/openmp" ] + +[dependencies.mkl-sys] +git = "https://github.com/Andlon/mkl-sys" +rev = "c197c97319f0784caae26012968c9c7bd76d494b" +features = [ "dss", "inspector-executor" ] + +[dev-dependencies] +approx = "0.3" + +# Make sure that mkl-sys compiles faster by compiling bindgen in release mode +[profile.dev.package.bindgen] +opt-level = 2 diff --git a/extern/mkl-corrode/README.md b/extern/mkl-corrode/README.md new file mode 100644 index 0000000..403ffee --- /dev/null +++ b/extern/mkl-corrode/README.md @@ -0,0 +1,4 @@ +# mkl-corrode +[![Build Status](https://github.com/Andlon/mkl-corrode/workflows/Build%20and%20run%20tests/badge.svg)](https://github.com/Andlon/mkl-sys/actions) + +A lightweight and pleasant Rust wrapper for Intel MKL. Tested on Ubuntu 18.04 and Windows Server 2019 with MKL 2019 Update 5 and MKL 2020 Update 1. diff --git a/extern/mkl-corrode/src/dss/mod.rs b/extern/mkl-corrode/src/dss/mod.rs new file mode 100644 index 0000000..02b1545 --- /dev/null +++ b/extern/mkl-corrode/src/dss/mod.rs @@ -0,0 +1,25 @@ +use mkl_sys::{MKL_DSS_NON_SYMMETRIC, MKL_DSS_SYMMETRIC, MKL_DSS_SYMMETRIC_STRUCTURE, MKL_INT}; + +mod solver; +mod sparse_matrix; +pub use solver::*; +pub use sparse_matrix::*; + +// TODO: Support complex numbers +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum MatrixStructure { + StructurallySymmetric, + Symmetric, + NonSymmetric, +} + +impl MatrixStructure { + fn to_mkl_opt(&self) -> MKL_INT { + use MatrixStructure::*; + match self { + StructurallySymmetric => MKL_DSS_SYMMETRIC_STRUCTURE, + Symmetric => MKL_DSS_SYMMETRIC, + NonSymmetric => MKL_DSS_NON_SYMMETRIC, + } + } +} diff --git a/extern/mkl-corrode/src/dss/solver.rs b/extern/mkl-corrode/src/dss/solver.rs new file mode 100644 index 0000000..6e2836d --- /dev/null +++ b/extern/mkl-corrode/src/dss/solver.rs @@ -0,0 +1,451 @@ +use mkl_sys::{ + _MKL_DSS_HANDLE_t, dss_create_, dss_define_structure_, dss_delete_, dss_factor_real_, + dss_reorder_, dss_solve_real_, MKL_DSS_AUTO_ORDER, MKL_DSS_BACKWARD_SOLVE, MKL_DSS_DEFAULTS, + MKL_DSS_DIAGONAL_SOLVE, MKL_DSS_FORWARD_SOLVE, MKL_DSS_INDEFINITE, MKL_DSS_METIS_OPENMP_ORDER, + MKL_DSS_POSITIVE_DEFINITE, MKL_DSS_ZERO_BASED_INDEXING, MKL_INT, +}; +use std::ffi::c_void; +use std::marker::PhantomData; +use std::ptr::{null, null_mut}; + +// MKL constants +use crate::dss::SparseMatrix; +use crate::SupportedScalar; +use core::fmt; +use mkl_sys::{ + MKL_DSS_COL_ERR, MKL_DSS_DIAG_ERR, MKL_DSS_FAILURE, MKL_DSS_I32BIT_ERR, MKL_DSS_INVALID_OPTION, + MKL_DSS_MSG_LVL_ERR, MKL_DSS_NOT_SQUARE, MKL_DSS_OOC_MEM_ERR, MKL_DSS_OOC_OC_ERR, + MKL_DSS_OOC_RW_ERR, MKL_DSS_OPTION_CONFLICT, MKL_DSS_OUT_OF_MEMORY, MKL_DSS_REORDER1_ERR, + MKL_DSS_REORDER_ERR, MKL_DSS_ROW_ERR, MKL_DSS_STATE_ERR, MKL_DSS_STATISTICS_INVALID_MATRIX, + MKL_DSS_STATISTICS_INVALID_STATE, MKL_DSS_STATISTICS_INVALID_STRING, MKL_DSS_STRUCTURE_ERR, + MKL_DSS_SUCCESS, MKL_DSS_TERM_LVL_ERR, MKL_DSS_TOO_FEW_VALUES, MKL_DSS_TOO_MANY_VALUES, + MKL_DSS_VALUES_ERR, +}; +use std::fmt::{Debug, Display}; + +/// Calls the given DSS function, noting its error code and upon a non-success result, +/// returns an appropriate error. +macro_rules! dss_call { + ($routine:ident ($($arg: tt)*)) => { + { + let code = $routine($($arg)*); + if code != MKL_DSS_SUCCESS { + return Err(Error::new(ErrorCode::from_return_code(code), stringify!($routine))); + } + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Error { + code: ErrorCode, + routine: &'static str, +} + +impl Error { + pub fn return_code(&self) -> ErrorCode { + self.code + } + + pub fn routine(&self) -> &str { + self.routine + } + + fn new(code: ErrorCode, routine: &'static str) -> Self { + Self { code, routine } + } +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!( + f, + "Error in routine {}. Return code: {:?}", + self.routine(), + self.return_code() + ) + } +} + +impl std::error::Error for Error {} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ErrorCode { + InvalidOption, + OutOfMemory, + MsgLvlErr, + TermLvlErr, + StateErr, + RowErr, + ColErr, + StructureErr, + NotSquare, + ValuesErr, + TooFewValues, + TooManyValues, + ReorderErr, + Reorder1Err, + I32BitErr, + Failure, + OptionConflict, + OocMemErr, + OocOcErr, + OocRwErr, + DiagErr, + StatisticsInvalidMatrix, + StatisticsInvalidState, + StatisticsInvalidString, + + /// Special error that does not exist in Intel MKL. + /// + /// This error is used when we encounter an unknown return code. This could for example + /// happen if a new version of Intel MKL adds more return codes and this crate has not + /// been updated to take that into account. + UnknownError, +} + +impl ErrorCode { + /// Construct a `DssError` from an MKL return code. + /// + /// This should cover every return code possible, but see notes made + /// in the docs for `UnknownError`. + fn from_return_code(code: MKL_INT) -> Self { + assert_ne!(code, MKL_DSS_SUCCESS); + + if code == MKL_DSS_INVALID_OPTION { + Self::InvalidOption + } else if code == MKL_DSS_OUT_OF_MEMORY { + Self::OutOfMemory + } else if code == MKL_DSS_MSG_LVL_ERR { + Self::MsgLvlErr + } else if code == MKL_DSS_TERM_LVL_ERR { + Self::TermLvlErr + } else if code == MKL_DSS_STATE_ERR { + Self::StateErr + } else if code == MKL_DSS_ROW_ERR { + Self::RowErr + } else if code == MKL_DSS_COL_ERR { + Self::ColErr + } else if code == MKL_DSS_STRUCTURE_ERR { + Self::StructureErr + } else if code == MKL_DSS_NOT_SQUARE { + Self::NotSquare + } else if code == MKL_DSS_VALUES_ERR { + Self::ValuesErr + } else if code == MKL_DSS_TOO_FEW_VALUES { + Self::TooFewValues + } else if code == MKL_DSS_TOO_MANY_VALUES { + Self::TooManyValues + } else if code == MKL_DSS_REORDER_ERR { + Self::ReorderErr + } else if code == MKL_DSS_REORDER1_ERR { + Self::Reorder1Err + } else if code == MKL_DSS_I32BIT_ERR { + Self::I32BitErr + } else if code == MKL_DSS_FAILURE { + Self::Failure + } else if code == MKL_DSS_OPTION_CONFLICT { + Self::OptionConflict + } else if code == MKL_DSS_OOC_MEM_ERR { + Self::OocMemErr + } else if code == MKL_DSS_OOC_OC_ERR { + Self::OocOcErr + } else if code == MKL_DSS_OOC_RW_ERR { + Self::OocRwErr + } else if code == MKL_DSS_DIAG_ERR { + Self::DiagErr + } else if code == MKL_DSS_STATISTICS_INVALID_MATRIX { + Self::StatisticsInvalidMatrix + } else if code == MKL_DSS_STATISTICS_INVALID_STATE { + Self::StatisticsInvalidState + } else if code == MKL_DSS_STATISTICS_INVALID_STRING { + Self::StatisticsInvalidString + } else { + Self::UnknownError + } + } +} + +/// A wrapper around _MKL_DSS_HANDLE_t. +/// +/// This is not exported from the library, but instead only used to simplify correct +/// destruction when a handle goes out of scope across the symbolic factorization +/// and numerical factorization. +struct Handle { + handle: _MKL_DSS_HANDLE_t, +} + +impl Handle { + fn create(options: MKL_INT) -> Result { + let mut handle = null_mut(); + unsafe { + dss_call! { dss_create_(&mut handle, &options) } + } + Ok(Self { handle }) + } +} + +impl Drop for Handle { + fn drop(&mut self) { + unsafe { + // TODO: Better handling here, but we cannot really do anything else than panic, + // can we? + let delete_opts = MKL_DSS_DEFAULTS; + let error = dss_delete_(&mut self.handle, &delete_opts); + if error != 0 { + panic!("dss_delete error: {}", error); + } + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Definiteness { + PositiveDefinite, + Indefinite, +} + +impl Definiteness { + fn to_mkl_opt(&self) -> MKL_INT { + use Definiteness::*; + match self { + PositiveDefinite => MKL_DSS_POSITIVE_DEFINITE, + Indefinite => MKL_DSS_INDEFINITE, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct SolverOptions { + parallel_reorder: bool, +} + +impl Default for SolverOptions { + fn default() -> Self { + Self { + parallel_reorder: false, + } + } +} + +impl SolverOptions { + pub fn parallel_reorder(self, enable: bool) -> Self { + Self { + parallel_reorder: enable, + ..self + } + } +} + +pub struct Solver { + handle: Handle, + marker: PhantomData, + num_rows: usize, + nnz: usize, +} + +impl Debug for Solver { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct(std::any::type_name::()) + .field("handle", &"") + .field("num_rows", &self.num_rows) + .field("nnz", &self.nnz) + .finish() + } +} + +impl Solver +where + T: SupportedScalar, +{ + pub fn try_factor_with_opts( + matrix: &SparseMatrix, + definiteness: Definiteness, + options: &SolverOptions, + ) -> Result { + let row_ptr = matrix.row_offsets(); + let columns = matrix.columns(); + let values = matrix.values(); + let structure = matrix.structure(); + let nnz = values.len(); + + // TODO: Part of error? + assert_eq!(values.len(), nnz); + + // TODO: Result instead of panic? + assert!(row_ptr.len() > 0); + let num_rows = row_ptr.len() - 1; + let num_cols = num_rows; + + // TODO: Enable tweaking messages! + let create_opts = MKL_DSS_DEFAULTS + MKL_DSS_ZERO_BASED_INDEXING; + let mut handle = Handle::create(create_opts)?; + + let define_opts = structure.to_mkl_opt(); + unsafe { + dss_call! { dss_define_structure_( + &mut handle.handle, + &define_opts, + row_ptr.as_ptr(), + // TODO: What if num_rows, nnz or num_cols > max(MKL_INT)? + &(num_rows as MKL_INT), + &(num_cols as MKL_INT), + columns.as_ptr(), + &(nnz as MKL_INT), + ) } + } + + let reorder_opts; + if options.parallel_reorder { + reorder_opts = MKL_DSS_METIS_OPENMP_ORDER; + } else { + reorder_opts = MKL_DSS_AUTO_ORDER; + } + unsafe { + dss_call! { dss_reorder_(&mut handle.handle, &reorder_opts, null()) } + }; + + let mut factorization = Solver { + handle, + num_rows, + nnz, + marker: PhantomData, + }; + factorization.refactor(values, definiteness)?; + Ok(factorization) + } + + /// Factors with default options. + pub fn try_factor(matrix: &SparseMatrix, definiteness: Definiteness) -> Result { + Self::try_factor_with_opts(matrix, definiteness, &SolverOptions::default()) + } + + pub fn refactor(&mut self, values: &[T], definiteness: Definiteness) -> Result<(), Error> { + // TODO: Part of error? + assert_eq!(values.len(), self.nnz); + + let opts = definiteness.to_mkl_opt(); + unsafe { + dss_call! { dss_factor_real_( + &mut self.handle.handle, + &opts, + values.as_ptr() as *const c_void, + ) } + }; + Ok(()) + } + + // TODO: Would it be safe to only take &self and still hand in a mutable pointer + // to the handle? We technically don't have any idea what is happening inside + // MKL, but on the other hand the factorization cannot be accessed from multiple threads, + // and I think as far as I can tell that the state of the factorization does not change? + // Unless an error somehow invalidates the handle? Not clear... + // Note: same for diagonal/backward + pub fn forward_substitute_into(&mut self, solution: &mut [T], rhs: &[T]) -> Result<(), Error> { + let num_rhs = rhs.len() / self.num_rows; + + // TODO: Make part of error? + assert_eq!( + rhs.len() % self.num_rows, + 0, + "Number of entries in RHS must be divisible by system size." + ); + assert_eq!(solution.len(), rhs.len()); + + unsafe { + dss_call! { + dss_solve_real_( + &mut self.handle.handle, + &(MKL_DSS_FORWARD_SOLVE), + rhs.as_ptr() as *const c_void, + // TODO: What if num_rhs > max(MKL_INT)? Absurd situation, but it could maybe + // lead to undefined behavior, so we need to handle it + &(num_rhs as MKL_INT), + solution.as_mut_ptr() as *mut c_void, + ) + } + }; + Ok(()) + } + + pub fn diagonal_substitute_into(&mut self, solution: &mut [T], rhs: &[T]) -> Result<(), Error> { + let num_rhs = rhs.len() / self.num_rows; + + // TODO: Make part of error? + assert_eq!( + rhs.len() % self.num_rows, + 0, + "Number of entries in RHS must be divisible by system size." + ); + assert_eq!(solution.len(), rhs.len()); + + unsafe { + dss_call! { + dss_solve_real_( + &mut self.handle.handle, + &(MKL_DSS_DIAGONAL_SOLVE), + rhs.as_ptr() as *const c_void, + // TODO: See other comment about this coercion cast + &(num_rhs as MKL_INT), + solution.as_mut_ptr() as *mut c_void, + ) + } + }; + Ok(()) + } + + pub fn backward_substitute_into(&mut self, solution: &mut [T], rhs: &[T]) -> Result<(), Error> { + let num_rhs = rhs.len() / self.num_rows; + + // TODO: Make part of error? + assert_eq!( + rhs.len() % self.num_rows, + 0, + "Number of entries in RHS must be divisible by system size." + ); + assert_eq!(solution.len(), rhs.len()); + + unsafe { + dss_call! { + dss_solve_real_( + &mut self.handle.handle, + &(MKL_DSS_BACKWARD_SOLVE), + rhs.as_ptr() as *const c_void, + // TODO: See other comment about num_rhs and `as` cast + &(num_rhs as MKL_INT), + solution.as_mut_ptr() as *mut c_void, + ) + } + }; + Ok(()) + } + + /// Convenience function for calling the different substitution phases. + /// + /// `buffer` must have same size as `solution`. + pub fn solve_into( + &mut self, + solution: &mut [T], + buffer: &mut [T], + rhs: &[T], + ) -> Result<(), Error> { + let y = solution; + self.forward_substitute_into(y, rhs)?; + + let z = buffer; + self.diagonal_substitute_into(z, &y)?; + + let x = y; + self.backward_substitute_into(x, &z)?; + + Ok(()) + } + + /// Convenience function that internally allocates buffer storage and output storage. + pub fn solve(&mut self, rhs: &[T]) -> Result, Error> { + let mut solution = vec![T::zero_element(); rhs.len()]; + let mut buffer = vec![T::zero_element(); rhs.len()]; + self.solve_into(&mut solution, &mut buffer, rhs)?; + Ok(solution) + } +} diff --git a/extern/mkl-corrode/src/dss/sparse_matrix.rs b/extern/mkl-corrode/src/dss/sparse_matrix.rs new file mode 100644 index 0000000..0e718cb --- /dev/null +++ b/extern/mkl-corrode/src/dss/sparse_matrix.rs @@ -0,0 +1,332 @@ +use crate::dss::MatrixStructure; +use crate::SupportedScalar; + +use mkl_sys::MKL_INT; + +use core::fmt; +use std::borrow::Cow; +use std::convert::TryFrom; +use std::fmt::{Debug, Display}; + +use crate::util::{is_same_type, transmute_identical_slice}; + +// TODO: We only care about square matrices +#[derive(Debug, PartialEq, Eq)] +pub struct SparseMatrix<'a, T> +where + T: Clone, +{ + row_offsets: Cow<'a, [MKL_INT]>, + columns: Cow<'a, [MKL_INT]>, + values: Cow<'a, [T]>, + structure: MatrixStructure, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SparseMatrixDataError { + NonMonotoneColumns, + MissingExplicitDiagonal, + UnexpectedLowerTriangularPart, + NonMonotoneRowOffsets, + EmptyRowOffsets, + InvalidRowOffset, + InvalidColumnIndex, + InsufficientIndexSize, +} + +impl SparseMatrixDataError { + fn is_recoverable(&self) -> bool { + use SparseMatrixDataError::*; + match self { + NonMonotoneColumns => false, + MissingExplicitDiagonal => true, + UnexpectedLowerTriangularPart => true, + NonMonotoneRowOffsets => false, + EmptyRowOffsets => false, + InvalidRowOffset => false, + InvalidColumnIndex => false, + InsufficientIndexSize => false, + } + } +} + +impl Display for SparseMatrixDataError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "Error in sparse matrix data: {:?}", self) + } +} + +impl std::error::Error for SparseMatrixDataError {} + +trait CsrProcessor { + /// Called when processing of the current row has finished. + fn row_processed(&mut self) {} + fn visit_column(&mut self, i: MKL_INT, j: MKL_INT, v: &T) -> Result<(), SparseMatrixDataError>; + fn visit_missing_diagonal_entry(&mut self, i: MKL_INT) -> Result<(), SparseMatrixDataError>; +} + +fn process_csr<'a, T, I>( + row_offsets: &'a [I], + columns: &'a [I], + values: &'a [T], + structure: MatrixStructure, + processor: &mut impl CsrProcessor, +) -> Result<(), SparseMatrixDataError> +where + T: SupportedScalar, + usize: TryFrom, + MKL_INT: TryFrom, + I: Copy, +{ + let needs_explicit_diagonal = match structure { + MatrixStructure::Symmetric | MatrixStructure::StructurallySymmetric => false, + MatrixStructure::NonSymmetric => true, + }; + + // Helper conversion functions. + let offset_as_usize = + |offset| usize::try_from(offset).map_err(|_| SparseMatrixDataError::InvalidRowOffset); + let index_as_mkl_int = + |idx| MKL_INT::try_from(idx).map_err(|_| SparseMatrixDataError::InvalidColumnIndex); + let usize_as_mkl_int = |idx| { + >::try_from(idx) + .map_err(|_| SparseMatrixDataError::InsufficientIndexSize) + }; + + let num_rows = row_offsets.len() - 1; + let num_cols = usize_as_mkl_int(num_rows)?; + let nnz = values.len(); + // TODO: Assertion or error? + assert_eq!(nnz, columns.len()); + + if row_offsets.is_empty() { + return Err(SparseMatrixDataError::EmptyRowOffsets); + } + + if nnz != offset_as_usize(*row_offsets.last().unwrap())? { + return Err(SparseMatrixDataError::InvalidRowOffset); + } + + for i in 0..num_rows { + let current_offset = row_offsets[i]; + let row_begin = offset_as_usize(current_offset)?; + let row_end = offset_as_usize(row_offsets[i + 1])?; + let i = usize_as_mkl_int(i)?; + + if row_end < row_begin { + return Err(SparseMatrixDataError::NonMonotoneRowOffsets); + } + + // - check that each column is in bounds, if not abort + // - check that column indices are monotone increasing, if not abort + // - If (structurally) symmetric: check that the diagonal element exists, if not insert it + // - If (structurally) symmetric: ignore lower triangular elements + + let columns_for_row = &columns[row_begin..row_end]; + let values_for_row = &values[row_begin..row_end]; + + // TODO: Rename to "have_processed" + let mut have_placed_diagonal = false; + let mut prev_column = None; + for (j, v_j) in columns_for_row.iter().zip(values_for_row) { + let j = index_as_mkl_int(*j)?; + + if j < 0 || j >= num_cols { + return Err(SparseMatrixDataError::InvalidColumnIndex); + } + + if let Some(j_prev) = prev_column { + if j <= j_prev { + return Err(SparseMatrixDataError::NonMonotoneColumns); + } + } + + if needs_explicit_diagonal { + if i == j { + have_placed_diagonal = true; + // TODO: Can remove the i < j comparison here! + } else if i < j && !have_placed_diagonal { + processor.visit_missing_diagonal_entry(i)?; + have_placed_diagonal = true; + } + } + + processor.visit_column(i, j, v_j)?; + prev_column = Some(j); + } + processor.row_processed(); + } + Ok(()) +} + +fn rebuild_csr<'a, T, I>( + row_offsets: &'a [I], + columns: &'a [I], + values: &'a [T], + structure: MatrixStructure, +) -> Result, SparseMatrixDataError> +where + T: SupportedScalar, + usize: TryFrom, + MKL_INT: TryFrom, + I: Copy, +{ + let keep_lower_tri = match structure { + MatrixStructure::Symmetric | MatrixStructure::StructurallySymmetric => false, + MatrixStructure::NonSymmetric => true, + }; + + struct CsrRebuilder { + new_row_offsets: Vec, + new_columns: Vec, + new_values: Vec, + current_offset: MKL_INT, + num_cols_in_current_row: MKL_INT, + keep_lower_tri: bool, + } + + impl CsrRebuilder { + fn push_val(&mut self, j: MKL_INT, v_j: X) { + self.new_columns.push(j); + self.new_values.push(v_j); + self.num_cols_in_current_row += 1; + } + } + + impl CsrProcessor for CsrRebuilder { + fn row_processed(&mut self) { + let new_offset = self.current_offset + self.num_cols_in_current_row; + self.current_offset = new_offset; + self.num_cols_in_current_row = 0; + self.new_row_offsets.push(new_offset); + } + + fn visit_column(&mut self, i: MKL_INT, j: MKL_INT, v_j: &X) -> Result<(), SparseMatrixDataError> { + let should_push = j >= i || (j < i && self.keep_lower_tri); + if should_push { + self.push_val(j, *v_j); + } + Ok(()) + } + + fn visit_missing_diagonal_entry(&mut self, i: MKL_INT) -> Result<(), SparseMatrixDataError> { + self.push_val(i, X::zero_element()); + Ok(()) + } + } + + let mut rebuilder = CsrRebuilder { + new_row_offsets: vec![0], + new_columns: Vec::new(), + new_values: Vec::new(), + current_offset: 0, + num_cols_in_current_row: 0, + keep_lower_tri, + }; + + process_csr(row_offsets, columns, values, structure, &mut rebuilder)?; + + let matrix = SparseMatrix { + row_offsets: Cow::Owned(rebuilder.new_row_offsets), + columns: Cow::Owned(rebuilder.new_columns), + values: Cow::Owned(rebuilder.new_values), + structure, + }; + Ok(matrix) +} + +impl<'a, T> SparseMatrix<'a, T> +where + T: SupportedScalar, +{ + pub fn row_offsets(&self) -> &[MKL_INT] { + &self.row_offsets + } + + pub fn columns(&self) -> &[MKL_INT] { + &self.columns + } + + pub fn values(&self) -> &[T] { + &self.values + } + + pub fn structure(&self) -> MatrixStructure { + self.structure + } + + pub fn try_from_csr( + row_offsets: &'a [MKL_INT], + columns: &'a [MKL_INT], + values: &'a [T], + structure: MatrixStructure, + ) -> Result { + let allow_lower_tri = match structure { + MatrixStructure::Symmetric | MatrixStructure::StructurallySymmetric => false, + MatrixStructure::NonSymmetric => true, + }; + + struct CsrCheck { + allow_lower_tri: bool, + } + + impl CsrProcessor for CsrCheck { + fn visit_column(&mut self, i: MKL_INT, j: MKL_INT, _: &X) -> Result<(), SparseMatrixDataError> { + if !self.allow_lower_tri && j < i { + Err(SparseMatrixDataError::UnexpectedLowerTriangularPart) + } else { + Ok(()) + } + } + + fn visit_missing_diagonal_entry( + &mut self, + _: MKL_INT, + ) -> Result<(), SparseMatrixDataError> { + Err(SparseMatrixDataError::MissingExplicitDiagonal) + } + } + + let mut checker = CsrCheck { allow_lower_tri }; + process_csr(row_offsets, columns, values, structure, &mut checker)?; + + let matrix = SparseMatrix { + row_offsets: Cow::Borrowed(row_offsets), + columns: Cow::Borrowed(columns), + values: Cow::Borrowed(values), + structure, + }; + Ok(matrix) + } + + pub fn try_convert_from_csr( + row_offsets: &'a [I], + columns: &'a [I], + values: &'a [T], + structure: MatrixStructure, + ) -> Result + where + I: 'static + Copy, + MKL_INT: TryFrom, + usize: TryFrom, + { + // If the data already has the right integer type, then try to pass it in to MKL directly. + // If it fails, it might be that we can recover by rebuilding the matrix data. + if is_same_type::() { + let row_offsets_mkl_int = transmute_identical_slice(row_offsets).unwrap(); + let columns_mkl_int = transmute_identical_slice(columns).unwrap(); + let result = + Self::try_from_csr(row_offsets_mkl_int, columns_mkl_int, values, structure); + match result { + Ok(matrix) => return Ok(matrix), + Err(error) => { + if !error.is_recoverable() { + return Err(error); + } + } + } + }; + + rebuild_csr(row_offsets, columns, values, structure) + } +} diff --git a/extern/mkl-corrode/src/extended_eigensolver.rs b/extern/mkl-corrode/src/extended_eigensolver.rs new file mode 100644 index 0000000..2aa81c9 --- /dev/null +++ b/extern/mkl-corrode/src/extended_eigensolver.rs @@ -0,0 +1,238 @@ +use crate::sparse::{CsrMatrixHandle, MatrixDescription, SparseStatusError}; +use crate::util::is_same_type; +use crate::SupportedScalar; + +use mkl_sys::{mkl_sparse_d_ev, mkl_sparse_d_svd, mkl_sparse_ee_init, MKL_INT}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EigenResult { + eigenvectors: Vec, + eigenvalues: Vec, + residuals: Vec, +} + +impl EigenResult { + pub fn eigenvalues(&self) -> &[T] { + &self.eigenvalues + } + + pub fn eigenvectors(&self) -> &[T] { + &self.eigenvectors + } + + pub fn residuals(&self) -> &[T] { + &self.residuals + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SvdResult { + singular_values: Vec, + left_vectors: Option>, + right_vectors: Option>, + residuals: Vec, +} + +impl SvdResult { + pub fn singular_values(&self) -> &[T] { + &self.singular_values + } + + pub fn left_vectors(&self) -> Option<&[T]> { + self.left_vectors.as_ref().map(|v| v.as_slice()) + } + + pub fn right_vectors(&self) -> Option<&[T]> { + self.right_vectors.as_ref().map(|v| v.as_slice()) + } + + pub fn residuals(&self) -> &[T] { + &self.residuals + } +} + +/// Decides whether to compute the smallest or largest eigenvalues/singular values. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Which { + Largest, + Smallest, +} + +impl Which { + fn integer_representation(&self) -> i8 { + match self { + Self::Largest => 'L' as i8, + Self::Smallest => 'S' as i8, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SingularVectorType { + Right, + Left, +} + +impl SingularVectorType { + fn integer_representation(&self) -> i8 { + match self { + Self::Right => 'R' as i8, + Self::Left => 'L' as i8, + } + } +} + +fn extremal_eigenvalues( + which: Which, + matrix: &CsrMatrixHandle, + description: &MatrixDescription, + k: usize, +) -> Result, SparseStatusError> +where + T: SupportedScalar, +{ + let k_in = k as MKL_INT; + let mut k_out = 0 as MKL_INT; + + if is_same_type::() { + // TODO: Allow tweaking options + let mut opts = vec![0 as MKL_INT; 128]; + let code = unsafe { mkl_sparse_ee_init(opts.as_mut_ptr()) }; + SparseStatusError::new_result(code, "mkl_sparse_ee_init")?; + + let mut eigenvalues = vec![T::zero_element(); k]; + let mut eigenvectors = vec![T::zero_element(); k * matrix.cols()]; + let mut residuals = vec![T::zero_element(); k]; + + let mut which = which.integer_representation(); + + let code = unsafe { + mkl_sparse_d_ev( + &mut which, + opts.as_mut_ptr(), + matrix.handle, + description.to_mkl_descr(), + k_in, + &mut k_out, + eigenvalues.as_mut_ptr() as *mut f64, + eigenvectors.as_mut_ptr() as *mut f64, + residuals.as_mut_ptr() as *mut f64, + ) + }; + SparseStatusError::new_result(code, "mkl_sparse_d_ev")?; + let k_out = k_out as usize; + eigenvalues.truncate(k_out); + eigenvectors.truncate(k_out * matrix.cols()); + residuals.truncate(k_out); + Ok(EigenResult { + eigenvectors, + eigenvalues, + residuals, + }) + } else { + panic!("Unsupported type"); + } +} + +pub fn sparse_svd( + which: Which, + vector_type: SingularVectorType, + matrix: &CsrMatrixHandle, + description: &MatrixDescription, + k: usize, +) -> Result, SparseStatusError> + where + T: SupportedScalar, +{ + // TODO: Check if k is not too large? + let k_in = k as MKL_INT; + let mut k_out = 0 as MKL_INT; + + if is_same_type::() { + // TODO: Allow tweaking options + let mut opts = vec![0 as MKL_INT; 128]; + let code = unsafe { mkl_sparse_ee_init(opts.as_mut_ptr()) }; + SparseStatusError::new_result(code, "mkl_sparse_ee_init")?; + + let mut singular_values = vec![T::zero_element(); k]; + let mut residuals = vec![T::zero_element(); k]; + let mut left_vectors = vec![T::zero_element(); k * matrix.rows()]; + let mut right_vectors = vec![T::zero_element(); k * matrix.cols()]; + + let mut which = which.integer_representation(); + let mut int_vector_type = vector_type.integer_representation(); + + let code = unsafe { + mkl_sparse_d_svd( + &mut which, + &mut int_vector_type, + opts.as_mut_ptr(), + matrix.handle, + description.to_mkl_descr(), + k_in, + &mut k_out, + singular_values.as_mut_ptr() as *mut f64, + left_vectors.as_mut_ptr() as *mut f64, + right_vectors.as_mut_ptr() as *mut f64, + residuals.as_mut_ptr() as *mut f64, + ) + }; + SparseStatusError::new_result(code, "mkl_sparse_d_svd")?; + let k_out = k_out as usize; + + singular_values.truncate(k_out); + residuals.truncate(k_out); + let mut result = SvdResult { + singular_values, + residuals, + left_vectors: None, + right_vectors: None, + }; + + match vector_type { + SingularVectorType::Left => { + left_vectors.truncate(k_out * matrix.rows()); + result.left_vectors = Some(left_vectors); + }, + SingularVectorType::Right => { + right_vectors.truncate(k_out * matrix.cols()); + result.right_vectors = Some(right_vectors); + } + } + + Ok(result) + } else { + panic!("Unsupported type"); + } +} + +/// Attempts to compute the `k` largest eigenvalues of the given matrix, with the given description. +/// +/// Note that the returned number of eigenvalues might be smaller than requested (see MKL +/// docs for details). +pub fn k_largest_eigenvalues( + matrix: &CsrMatrixHandle, + description: &MatrixDescription, + k: usize, +) -> Result, SparseStatusError> +where + T: SupportedScalar, +{ + extremal_eigenvalues(Which::Largest, matrix, description, k) +} + +/// Attempts to compute the `k` smallest eigenvalues of the given matrix, with the given description. +/// +/// Note that the returned number of eigenvalues might be smaller than requested (see MKL +/// docs for details). +// TODO: Extend to general sparse matrices, not just CSR +pub fn k_smallest_eigenvalues( + matrix: &CsrMatrixHandle, + description: &MatrixDescription, + k: usize, +) -> Result, SparseStatusError> +where + T: SupportedScalar, +{ + extremal_eigenvalues(Which::Smallest, matrix, description, k) +} diff --git a/extern/mkl-corrode/src/lib.rs b/extern/mkl-corrode/src/lib.rs new file mode 100644 index 0000000..497ad1a --- /dev/null +++ b/extern/mkl-corrode/src/lib.rs @@ -0,0 +1,34 @@ +pub extern crate mkl_sys; + +pub mod dss; +pub mod extended_eigensolver; +pub mod sparse; + +mod util; + +mod internal { + pub trait InternalScalar { + fn zero_element() -> Self; + fn try_as_f64(&self) -> Option; + } +} + +/// Marker trait for supported scalar types. +/// +/// Can not be implemented by dependent crates. +pub unsafe trait SupportedScalar: 'static + Copy + internal::InternalScalar {} + +// TODO: To support f32 we need to pass appropriate options during handle creation +// Can have the sealed trait provide us with the appropriate option for this! +//impl private::Sealed for f32 {} +impl internal::InternalScalar for f64 { + fn zero_element() -> Self { + 0.0 + } + + fn try_as_f64(&self) -> Option { + Some(*self) + } +} +//unsafe impl SupportedScalar for f32 {} +unsafe impl SupportedScalar for f64 {} diff --git a/extern/mkl-corrode/src/sparse.rs b/extern/mkl-corrode/src/sparse.rs new file mode 100644 index 0000000..1ccfce2 --- /dev/null +++ b/extern/mkl-corrode/src/sparse.rs @@ -0,0 +1,402 @@ +use crate::util::{is_same_type, transmute_identical_slice, transmute_identical_slice_mut}; +use mkl_sys::{ + matrix_descr, mkl_sparse_d_create_csr, mkl_sparse_destroy, + mkl_sparse_d_mv, mkl_sparse_set_mv_hint, + sparse_diag_type_t, sparse_fill_mode_t, sparse_matrix_t, sparse_matrix_type_t, + sparse_operation_t, mkl_sparse_optimize, + MKL_INT, +}; +use std::marker::PhantomData; +use std::ptr::{null_mut}; +use std::convert::TryFrom; + +use crate::SupportedScalar; +use mkl_sys::sparse_status_t; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SparseStatusCode { + NotInitialized, + InvalidValue, + InternalError, + + // TODO: More errors + /// Special enum variant that corresponds to an error returned by MKL that is not recognized + /// by the `mkl-corrode` crate. + /// + /// This can happen if e.g. a new version of MKL adds new possible return values. + /// The integer returned is the status code that was not recognized. + UnknownError(sparse_status_t::Type), +} + +impl SparseStatusCode { + pub fn from_raw_code(status: sparse_status_t::Type) -> SparseStatusCode { + assert_ne!(status, sparse_status_t::SPARSE_STATUS_SUCCESS); + use sparse_status_t::*; + use SparseStatusCode::*; + + if status == SPARSE_STATUS_NOT_INITIALIZED { + NotInitialized + } else if status == SPARSE_STATUS_INVALID_VALUE { + InvalidValue + } else if status == SPARSE_STATUS_INTERNAL_ERROR { + InternalError + } else { + UnknownError(status) + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SparseStatusError { + code: SparseStatusCode, + routine: &'static str, +} + +impl SparseStatusError { + // TODO: pub (crate) does not look so nice. Rather reorganize modules? + pub(crate) fn new_result( + code: sparse_status_t::Type, + routine: &'static str, + ) -> Result<(), Self> { + if code == sparse_status_t::SPARSE_STATUS_SUCCESS { + Ok(()) + } else { + Err(Self { + code: SparseStatusCode::from_raw_code(code), + routine, + }) + } + } + + pub fn code(&self) -> SparseStatusCode { + self.code + } + + pub fn routine(&self) -> &str { + &self.routine + } +} + +pub struct CsrMatrixHandle<'a, T> { + marker: PhantomData<&'a T>, + rows: usize, + cols: usize, + nnz: usize, + pub(crate) handle: sparse_matrix_t, +} + +impl<'a, T> Drop for CsrMatrixHandle<'a, T> { + fn drop(&mut self) { + unsafe { + // TODO: Does MKL actually take ownership of the arrays in _create_csr? + // In other words, will this try to deallocate the memory of the matrices passed in + // as slices? If so, that would be disastrous. The Intel MKL docs are as usual + // not clear on this + let status = mkl_sparse_destroy(self.handle); + if SparseStatusError::new_result(status, "mkl_sparse_destroy").is_err() { + // TODO: Should we panic here? Or just print to eprintln!? + // I'd venture that if this fails, then there's something seriously wrong + // somewhere... + panic!("Error during sparse matrix destruction.") + }; + } + } +} + +impl<'a, T> CsrMatrixHandle<'a, T> +where + T: SupportedScalar, +{ + pub fn rows(&self) -> usize { + self.rows + } + + pub fn cols(&self) -> usize { + self.cols + } + + pub fn nnz(&self) -> usize { + self.nnz + } + + pub fn from_csr_data( + rows: usize, + cols: usize, + row_begin: &'a [MKL_INT], + row_end: &'a [MKL_INT], + columns: &'a [MKL_INT], + values: &'a [T] + ) -> Result { + assert_eq!(row_begin.len(), rows, "row_begin length and rows must be equal"); + assert_eq!(row_end.len(), rows, "row_end length and rows must be equal"); + assert_eq!(columns.len(), values.len(), "columns and values must have equal length"); + + assert_eq!(row_begin.first().unwrap_or(&0), &0); + assert!(row_end.first().unwrap_or(&0) >= &0); + + let is_monotonic = |slice: &[_]| (1 .. slice.len()) + .all(|row_idx| slice[row_idx] >= slice[row_idx - 1]); + assert!(is_monotonic(row_begin)); + assert!(is_monotonic(row_end)); + + // Since the numbers are monotonic, it follows that the last element is + // non-negative, and therefore fits inside a usize + assert_eq!(row_end.last().copied().unwrap_or(0) as usize, values.len()); + + // TODO: Do column indices in each row need to be sorted? I don't think so... + // MKL docs don't say that in the description of the format, at least. + assert!(columns.iter().all(|index| index >= &0 && (*index as usize) < cols), + "column indices must lie in the interval [0, cols)"); + + unsafe { + Self::from_raw_csr_data(rows, cols, row_begin, row_end, columns, values) + } + } + + // TODO: Apparently this routine is only supported for BSR according to Intel docs? + // That's very disappointing... + // pub fn update_values(&mut self, new_values: &[T]) -> Result<(), SparseStatusError> { + // assert_eq!(self.nnz(), new_values.len(), + // "Number of new values must be consistent with matrix"); + // + // unsafe { + // if is_same_type::() { + // let status = mkl_sparse_d_update_values( + // self.handle, + // // TODO: This isn't necessarily technically safe, because MKL_INT + // // may be 32-bit and new_values might exceed the size of MKL_INT + // // on 64-bit platforms + // new_values.len() as MKL_INT, + // null(), + // null(), + // // TODO: This looks very bad, but MKL does not have a const pointer in its API, + // // so we have to trust them that they do not modify the array. + // new_values.as_ptr() as *mut f64 + // ); + // SparseStatusError::new_result(status, "mkl_sparse_d_update_values")?; + // Ok(()) + // } else { + // // TODO: Implement more types + // panic!("Unsupported type") + // } + // } + // } + + /// TODO: Change this to be more general? + /// TODO: Build safe abstraction on top + pub unsafe fn from_raw_csr_data( + rows: usize, + cols: usize, + row_begin: &'a [MKL_INT], + row_end: &'a [MKL_INT], + columns: &'a [MKL_INT], + values: &'a [T], + ) -> Result { + // TODO: Handle this more properly + let rows_mkl = rows as MKL_INT; + let cols_mkl = cols as MKL_INT; + + let mut handle = null_mut(); + if is_same_type::() { + // Note: According to + // https://software.intel.com/en-us/mkl-developer-reference-c-mkl-sparse-create-csr + // MKL does not modify the input arrays UNLESS we call mkl_sparse_order, + // so it should be safe to borrow the data as long as we don't do that. + let status = mkl_sparse_d_create_csr( + &mut handle, + 0, + rows_mkl, + cols_mkl, + row_begin.as_ptr() as *mut _, + row_end.as_ptr() as *mut _, + columns.as_ptr() as *mut _, + values.as_ptr() as *mut _, + ); + SparseStatusError::new_result(status, "mkl_sparse_d_create_csr")?; + Ok(Self { + marker: PhantomData, + rows, + cols, + handle, + nnz: values.len() + }) + } else { + // TODO: Implement more types + panic!("Unsupported type") + } + } + + // TODO: Is it correct that this does not take self by mut ref? I think it's tantamount + // to MKL just modifying some internal cached variables, but not the data itself...? + pub fn set_mv_hint(&self, operation: SparseOperation, + description: &MatrixDescription, + expected_calls: usize) + -> Result<(), SparseStatusError> + { + if is_same_type::() { + unsafe { + let status = mkl_sparse_set_mv_hint( + self.handle, + operation.to_mkl_value(), + description.to_mkl_descr(), + MKL_INT::try_from(expected_calls) + .expect("TODO: How to deal with numbers that don't fit in MKL_INT?") + ); + SparseStatusError::new_result(status, "mkl_sparse_set_mv_hint")?; + Ok(()) + } + } else { + panic!("Unsupported type"); + } + } + + pub fn optimize(&self) -> Result<(), SparseStatusError> { + if is_same_type::() { + unsafe { + let status = mkl_sparse_optimize(self.handle); + SparseStatusError::new_result(status, "mkl_sparse_optimize")?; + Ok(()) + } + } else { + panic!("Unsupported type"); + } + } +} + +pub enum SparseOperation { + NonTranspose, + Transpose, + ConjugateTranspose +} + +impl SparseOperation { + fn to_mkl_value(&self) -> sparse_operation_t::Type { + match self { + Self::NonTranspose => sparse_operation_t::SPARSE_OPERATION_NON_TRANSPOSE, + Self::Transpose => sparse_operation_t::SPARSE_OPERATION_TRANSPOSE, + Self::ConjugateTranspose => sparse_operation_t::SPARSE_OPERATION_CONJUGATE_TRANSPOSE + } + } +} + +/// y <- alpha * op(matrix) * x + beta * y +pub fn spmv_csr(operation: SparseOperation, + alpha: T, + matrix: &CsrMatrixHandle, + description: &MatrixDescription, + x: &[T], + beta: T, + y: &mut [T]) +-> Result<(), SparseStatusError> +where + T: SupportedScalar +{ + assert_eq!(y.len(), matrix.rows(), + "Number of rows of matrix must be identical to length of y."); + assert_eq!(x.len(), matrix.cols(), + "Number of columns of matrix must be identical to length of y."); + if is_same_type::() { + unsafe { + let status = mkl_sparse_d_mv( + operation.to_mkl_value(), + alpha.try_as_f64().unwrap(), + matrix.handle, + description.to_mkl_descr(), + transmute_identical_slice(x).unwrap().as_ptr(), + beta.try_as_f64().unwrap(), + transmute_identical_slice_mut(y).unwrap().as_mut_ptr() + ); + SparseStatusError::new_result(status, "mkl_sparse_d_mv")?; + Ok(()) + } + } else { + panic!("Unsupported type"); + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SparseMatrixType { + General, + Symmetric, +} + +impl SparseMatrixType { + fn to_mkl_value(&self) -> sparse_matrix_type_t::Type { + match self { + SparseMatrixType::General => sparse_matrix_type_t::SPARSE_MATRIX_TYPE_GENERAL, + SparseMatrixType::Symmetric => sparse_matrix_type_t::SPARSE_MATRIX_TYPE_SYMMETRIC, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SparseFillMode { + Lower, + Upper, +} + +impl SparseFillMode { + fn to_mkl_value(&self) -> sparse_fill_mode_t::Type { + match self { + SparseFillMode::Lower => sparse_fill_mode_t::SPARSE_FILL_MODE_LOWER, + SparseFillMode::Upper => sparse_fill_mode_t::SPARSE_FILL_MODE_UPPER, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SparseDiagType { + NonUnit, + Unit, +} + +impl SparseDiagType { + fn to_mkl_value(&self) -> sparse_diag_type_t::Type { + match self { + SparseDiagType::NonUnit => sparse_diag_type_t::SPARSE_DIAG_NON_UNIT, + SparseDiagType::Unit => sparse_diag_type_t::SPARSE_DIAG_UNIT, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct MatrixDescription { + matrix_type: SparseMatrixType, + fill_mode: SparseFillMode, + diag_type: SparseDiagType, +} + +impl Default for MatrixDescription { + fn default() -> Self { + Self { + matrix_type: SparseMatrixType::General, + fill_mode: SparseFillMode::Upper, + diag_type: SparseDiagType::NonUnit, + } + } +} + +impl MatrixDescription { + pub fn with_type(self, matrix_type: SparseMatrixType) -> Self { + Self { + matrix_type, + ..self + } + } + + pub fn with_fill_mode(self, fill_mode: SparseFillMode) -> Self { + Self { fill_mode, ..self } + } + + pub fn with_diag_type(self, diag_type: SparseDiagType) -> Self { + Self { diag_type, ..self } + } + + pub(crate) fn to_mkl_descr(&self) -> matrix_descr { + matrix_descr { + type_: self.matrix_type.to_mkl_value(), + mode: self.fill_mode.to_mkl_value(), + diag: self.diag_type.to_mkl_value(), + } + } +} diff --git a/extern/mkl-corrode/src/util.rs b/extern/mkl-corrode/src/util.rs new file mode 100644 index 0000000..b1855a9 --- /dev/null +++ b/extern/mkl-corrode/src/util.rs @@ -0,0 +1,34 @@ +use std::any::TypeId; +use std::mem::transmute; + +pub fn is_same_type() -> bool +where + T: 'static, + U: 'static, +{ + TypeId::of::() == TypeId::of::() +} + +pub fn transmute_identical_slice(slice: &[T]) -> Option<&[U]> +where + T: 'static, + U: 'static, +{ + if is_same_type::() { + Some(unsafe { transmute(slice) }) + } else { + None + } +} + +pub fn transmute_identical_slice_mut(slice: &mut [T]) -> Option<&mut [U]> + where + T: 'static, + U: 'static, +{ + if is_same_type::() { + Some(unsafe { transmute(slice) }) + } else { + None + } +} diff --git a/extern/mkl-corrode/tests/integration.rs b/extern/mkl-corrode/tests/integration.rs new file mode 100644 index 0000000..8f4d605 --- /dev/null +++ b/extern/mkl-corrode/tests/integration.rs @@ -0,0 +1,324 @@ +use mkl_corrode::dss::{Definiteness, MatrixStructure, Solver, SparseMatrix}; + +use approx::assert_abs_diff_eq; + +use mkl_corrode::dss::Definiteness::Indefinite; +use mkl_corrode::dss::MatrixStructure::NonSymmetric; +use mkl_corrode::extended_eigensolver::{k_largest_eigenvalues, k_smallest_eigenvalues, sparse_svd, Which, SingularVectorType}; +use mkl_corrode::sparse::{CsrMatrixHandle, MatrixDescription, SparseMatrixType, spmv_csr, SparseOperation}; +use Definiteness::PositiveDefinite; +use MatrixStructure::Symmetric; + +#[test] +fn dss_1x1_factorization() { + let row_ptr = [0, 1]; + let columns = [0]; + let values = [2.0]; + + let matrix = + SparseMatrix::try_convert_from_csr(&row_ptr, &columns, &values, Symmetric).unwrap(); + let mut fact = Solver::try_factor(&matrix, PositiveDefinite).unwrap(); + + let rhs = [2.0]; + let mut sol = [0.0]; + let mut buffer = [0.0]; + fact.solve_into(&mut sol, &mut buffer, &rhs).unwrap(); + + let expected_sol = [1.0]; + assert_abs_diff_eq!(sol.as_ref(), expected_sol.as_ref(), epsilon = 1e-12); +} + +#[test] +fn dss_factorization() { + // Matrix: + // [10, 0, 2, 7, + // 3, 6, 0, 0, + // 0, 7, 9, 1, + // 0, 2, 0, 3] + + let row_ptr = [0, 3, 5, 8, 10]; + let columns = [0, 2, 3, 0, 1, 1, 2, 3, 1, 3]; + let values = [10.0, 2.0, 7.0, 3.0, 6.0, 7.0, 9.0, 1.0, 2.0, 3.0]; + + let matrix = + SparseMatrix::try_convert_from_csr(&row_ptr, &columns, &values, NonSymmetric).unwrap(); + let mut fact = Solver::try_factor(&matrix, Indefinite).unwrap(); + + let rhs = [7.0, -13.0, 2.0, -1.0]; + let mut sol = [0.0, 0.0, 0.0, 0.0]; + let mut buffer = sol.clone(); + fact.solve_into(&mut sol, &mut buffer, &rhs).unwrap(); + let expected_sol = [-(1.0 / 3.0), -2.0, 5.0 / 3.0, 1.0]; + + assert_abs_diff_eq!(sol.as_ref(), expected_sol.as_ref(), epsilon = 1e-12); +} + +#[test] +fn dss_symmetric_posdef_factorization() { + // Redundantly stored entries (i.e. lower triangular portion explicitly stored + { + // Matrix + // [10, 0, 2, + // 0, 5, 1 + // 2 1 4] + let row_ptr = [0, 2, 4, 7]; + let columns = [0, 2, 1, 2, 0, 1, 2]; + let values = [10.0, 2.0, 5.0, 1.0, 2.0, 1.0, 4.0]; + + let matrix = + SparseMatrix::try_convert_from_csr(&row_ptr, &columns, &values, Symmetric).unwrap(); + let mut fact = Solver::try_factor(&matrix, PositiveDefinite).unwrap(); + + let rhs = [2.0, -3.0, 5.0]; + let solution = fact.solve(&rhs).unwrap(); + let expected_sol = [-0.10588235, -0.90588235, 1.52941176]; + + assert_abs_diff_eq!(solution.as_ref(), expected_sol.as_ref(), epsilon = 1e-6); + } + + // Same test, but store only upper triangular part of matrix + { + // Matrix + // [10, 0, 2, + // 0, 5, 1 + // 2 1 4] + let row_ptr = [0, 2, 4, 5]; + let columns = [0, 2, 1, 2, 2]; + let values = [10.0, 2.0, 5.0, 1.0, 4.0]; + + let matrix = + SparseMatrix::try_convert_from_csr(&row_ptr, &columns, &values, Symmetric).unwrap(); + let mut fact = Solver::try_factor(&matrix, PositiveDefinite).unwrap(); + + let rhs = [2.0, -3.0, 5.0]; + let solution = fact.solve(&rhs).unwrap(); + let expected_sol = [-0.10588235, -0.90588235, 1.52941176]; + + assert_abs_diff_eq!(solution.as_ref(), expected_sol.as_ref(), epsilon = 1e-6); + } +} + +#[test] +fn csr_unsafe_construction_destruction() { + // Matrix + // [10, 0, 2, + // 0, 5, 1 + // 2 1 4] + let row_ptr = [0, 2, 4, 7]; + let columns = [0, 2, 1, 2, 0, 1, 2]; + let values = [10.0, 2.0, 5.0, 1.0, 2.0, 1.0, 4.0]; + + let matrix = CsrMatrixHandle::from_csr_data( + 3, + 3, + &row_ptr[..row_ptr.len() - 1], + &row_ptr[1..], + &columns, + &values, + ).unwrap(); + drop(matrix); + + // Check that dropping the handle does not "destroy" the input data + // (note: it may be necessary to run this test through Valgrind and/or adress/memory sanitizers + // to make sure that it works as intended. + assert_eq!(row_ptr, [0, 2, 4, 7]); + assert_eq!(columns, [0, 2, 1, 2, 0, 1, 2]); + assert_eq!(values, [10.0, 2.0, 5.0, 1.0, 2.0, 1.0, 4.0]); +} + +#[test] +fn basic_k_smallest_largest_eigenvalues() { + // Matrix + // [10, 0, 2, + // 0, 5, 1 + // 2 1 4] + let row_ptr = [0, 2, 4, 7]; + let columns = [0, 2, 1, 2, 0, 1, 2]; + let values = [10.0, 2.0, 5.0, 1.0, 2.0, 1.0, 4.0]; + let matrix = CsrMatrixHandle::from_csr_data( + 3, + 3, + &row_ptr[..row_ptr.len() - 1], + &row_ptr[1..], + &columns, + &values, + ).unwrap(); + + let description = MatrixDescription::default().with_type(SparseMatrixType::General); + let expected_eigvals = vec![2.94606902, 5.43309508, 10.6208359]; + let largest1 = k_largest_eigenvalues(&matrix, &description, 1).unwrap(); + let largest2 = k_largest_eigenvalues(&matrix, &description, 2).unwrap(); + let largest3 = k_largest_eigenvalues(&matrix, &description, 3).unwrap(); + + assert_abs_diff_eq!( + largest1.eigenvalues(), + &expected_eigvals[2..=2], + epsilon = 1e-6 + ); + + assert_abs_diff_eq!( + largest2.eigenvalues(), + &expected_eigvals[1..=2], + epsilon = 1e-6 + ); + + assert_abs_diff_eq!( + largest3.eigenvalues(), + &expected_eigvals[0..=2], + epsilon = 1e-6 + ); + + let smallest1 = k_smallest_eigenvalues(&matrix, &description, 1).unwrap(); + let smallest2 = k_smallest_eigenvalues(&matrix, &description, 2).unwrap(); + let smallest3 = k_smallest_eigenvalues(&matrix, &description, 3).unwrap(); + + assert_abs_diff_eq!( + smallest1.eigenvalues(), + &expected_eigvals[0..=0], + epsilon = 1e-6 + ); + + assert_abs_diff_eq!( + smallest2.eigenvalues(), + &expected_eigvals[0..=1], + epsilon = 1e-6 + ); + + assert_abs_diff_eq!( + smallest3.eigenvalues(), + &expected_eigvals[0..=2], + epsilon = 1e-6 + ); +} + +#[test] +fn basic_sparse_svd() { + // Matrix + // [10, -5, 0, + // 0, 5, 1 + // 2 0 4] + let row_ptr = [0, 2, 4, 6]; + let columns = [0, 1, 1, 2, 0, 2]; + let values = [10.0, -5.0, 5.0, 1.0, 2.0, 4.0]; + let matrix = CsrMatrixHandle::from_csr_data( + 3, + 3, + &row_ptr[..row_ptr.len() - 1], + &row_ptr[1..], + &columns, + &values, + ).unwrap(); + + let description = MatrixDescription::default(); + + // "All" eigenvalues + { + let result = sparse_svd(Which::Largest, + SingularVectorType::Left, + &matrix, + &description, + 3) + .unwrap(); + + let expected_singular_values = vec![ + 3.155542242601061, 5.201796372629078, 11.575140070550471 + ]; + + let mut sorted_values = result.singular_values().to_vec(); + sorted_values.sort_by(|a, b| a.partial_cmp(&b).unwrap()); + + assert_abs_diff_eq!( + sorted_values.as_slice(), + expected_singular_values.as_slice(), + epsilon = 1e-9 + ); + } + + // Get smallest eigenvalue + { + let result = sparse_svd(Which::Smallest, + SingularVectorType::Left, + &matrix, + &description, + 1) + .unwrap(); + + assert_abs_diff_eq!(result.singular_values()[0], 3.155542242601061, epsilon=1e-9); + } + + // Get largest eigenvalue + { + let result = sparse_svd(Which::Largest, + SingularVectorType::Left, + &matrix, + &description, + 1) + .unwrap(); + + assert_abs_diff_eq!(result.singular_values()[0], 11.575140070550471, epsilon=1e-9); + } + + // TODO: Test singular vectors + +} + +#[test] +fn dss_solver_debug() { + use std::fmt::Write; + + let row_ptr = [0, 1]; + let columns = [0]; + let values = [0.0]; + + // Construct dummy matrix + let matrix = SparseMatrix::try_convert_from_csr(&row_ptr, &columns, &values, NonSymmetric) + .unwrap(); + let solver = Solver::try_factor(&matrix, Indefinite).unwrap(); + + let mut debug_str = String::new(); + write!(&mut debug_str, "{:?}", solver).unwrap(); + + assert_eq!(debug_str, + "mkl_corrode::dss::solver::Solver { handle: \"\", num_rows: 1, nnz: 1 }"); +} + +#[test] +fn sparse_spmv_csr_plus_update() { + // Matrix: + // [10, 0, 2, 7, + // 3, 6, 0, 0, + // 0, 7, 9, 1, + // 0, 2, 0, 3] + + let row_ptr = [0, 3, 5, 8, 10]; + let columns = [0, 2, 3, 0, 1, 1, 2, 3, 1, 3]; + let values = [10.0, 2.0, 7.0, 3.0, 6.0, 7.0, 9.0, 1.0, 2.0, 3.0]; + let csr = CsrMatrixHandle::from_csr_data(4, 4, + &row_ptr[..row_ptr.len() - 1], + &row_ptr[1..], &columns, &values).unwrap(); + + let alpha = 2.0; + let x = [3.0, -2.0, 1.0, 5.0]; + let beta = 3.0; + let mut y = [2.0, 3.0, 1.0, -4.0]; + let description = MatrixDescription::default(); + spmv_csr(SparseOperation::NonTranspose, alpha, &csr, &description, &x, beta, &mut y).unwrap(); + + assert_abs_diff_eq!(y[0], 140.0, epsilon=1e-14); + assert_abs_diff_eq!(y[1], 3.0, epsilon=1e-14); + assert_abs_diff_eq!(y[2], 3.0, epsilon=1e-14); + assert_abs_diff_eq!(y[3], 10.0, epsilon=1e-14); + + // TODO: Re-enable these tests if this ever becomes possible in the future. + // Currently there seems to be no way to directly update values of a CSR matrix (only BSR). + // Try to update values and re-run the operation + // let new_values = [8.0, 4.0, 3.0, 2.0, 6.0, -5.0, 8.0, -1.0, 2.0, -4.0]; + // csr.update_values(&new_values).unwrap(); + // let mut y = [2.0, 3.0, 1.0, -4.0]; + // spmv_csr(SparseOperation::NonTranspose, alpha, &csr, &description, &x, beta, &mut y).unwrap(); + // + // assert_abs_diff_eq!(y[0], 92.0, epsilon=1e-14); + // assert_abs_diff_eq!(y[1], -3.0, epsilon=1e-14); + // assert_abs_diff_eq!(y[2], 29.0, epsilon=1e-14); + // assert_abs_diff_eq!(y[3], 20.0, epsilon=1e-14); +} diff --git a/extern/mkl-sys/.github/workflows/build_and_run.yml b/extern/mkl-sys/.github/workflows/build_and_run.yml new file mode 100644 index 0000000..6b4fa7e --- /dev/null +++ b/extern/mkl-sys/.github/workflows/build_and_run.yml @@ -0,0 +1,274 @@ +name: Build and run tests + +on: + # Trigger the workflow on push or pull request, + # but only for the master branch + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build_ubuntu_mkl2019: + + name: Test on Ubuntu (MKL 2019.5) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install MKL + run: | + wget https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS-2019.PUB + sudo apt-key add GPG-PUB-KEY-INTEL-SW-PRODUCTS-2019.PUB + sudo sh -c 'echo deb https://apt.repos.intel.com/mkl all main > /etc/apt/sources.list.d/intel-mkl.list' + sudo apt update + sudo apt install intel-mkl-64bit-2020.0-088 + + - name: Update Rust + run: rustup update + + - name: Build (--features "dss") + run: | + source /opt/intel/mkl/bin/mklvars.sh intel64 + cargo build --features "dss" --verbose + - name: Run tests (--features "dss") + run: | + source /opt/intel/mkl/bin/mklvars.sh intel64 + cargo test --features "dss" --verbose + - name: Build (--release --features "all") + run: | + source /opt/intel/mkl/bin/mklvars.sh intel64 + cargo build --release --features "all" --verbose + - name: Run tests (--release --features "all") + run: | + source /opt/intel/mkl/bin/mklvars.sh intel64 + cargo test --release --features "all" --verbose + + + build_ubuntu_mkl2020: + + name: Test on Ubuntu (MKL 2020.1) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install MKL + run: | + wget https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS-2019.PUB + sudo apt-key add GPG-PUB-KEY-INTEL-SW-PRODUCTS-2019.PUB + sudo sh -c 'echo deb https://apt.repos.intel.com/mkl all main > /etc/apt/sources.list.d/intel-mkl.list' + sudo apt update + sudo apt install intel-mkl-64bit-2020.1-102 + + - name: Update Rust + run: rustup update + + - name: Build (--features "dss") + run: | + source /opt/intel/mkl/bin/mklvars.sh intel64 + cargo build --features "dss" --verbose + - name: Run tests (--features "dss") + run: | + source /opt/intel/mkl/bin/mklvars.sh intel64 + cargo test --features "dss" --verbose + - name: Build (--release --features "all") + run: | + source /opt/intel/mkl/bin/mklvars.sh intel64 + cargo build --release --features "all" --verbose + - name: Run tests (--release --features "all") + run: | + source /opt/intel/mkl/bin/mklvars.sh intel64 + cargo test --release --features "all" --verbose + + + build_windows_mkl2019: + + name: Test on Windows (MKL 2019.5) + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + + # Caching of MKL and clang does not work due to cache size limitations of Github + + # - name: Cache MKL installation + # id: cache-mkl-installation + # uses: actions/cache@v1 + # with: + # path: mkl + # key: ${{ runner.os }}-mkl-installed-w_mkl_2019.5.281 + - name: Download MKL installer + #if: steps.cache-mkl-installation.outputs.cache-hit != 'true' + run: (New-Object System.Net.WebClient).DownloadFile("http://registrationcenter-download.intel.com/akdlm/irc_nas/tec/15806/w_mkl_2019.5.281.exe", "$(($pwd).path)\w_mkl_2019.5.281.exe") + shell: pwsh + - name: Extract MKL installer files + #if: steps.cache-mkl-installation.outputs.cache-hit != 'true' + run: Start-Process .\w_mkl_2019.5.281.exe -ArgumentList @("--silent", "--extract-folder", "$(($pwd).path)\mkl-installer", "--extract-only") -Wait + shell: pwsh + - name: Install MKL + #if: steps.cache-mkl-installation.outputs.cache-hit != 'true' + run: Start-Process .\mkl-installer\install.exe -ArgumentList @("install", "--output=$(($pwd).path)\log.txt", "--eula=accept", "--installdir=$(($pwd).path)\mkl") -Wait + shell: pwsh + # - name: Show log + # if: steps.cache-mkl-installation.outputs.cache-hit != 'true' + # run: gc "$(($pwd).path)\log.txt" + # shell: pwsh + # - name: Find "mklvars.bat" + # run: ls -r -ea silentlycontinue -fo -inc "mklvars.bat" | % { $_.fullname } + # shell: pwsh + # - name: Run mklvars.bat + # run: .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + # shell: pwsh + + # - name: Cache Clang installation + # id: cache-clang-installation + # uses: actions/cache@v1 + # with: + # path: clang + # key: ${{ runner.os }}-clang-installed-LLVM-9.0.0-win64 + - name: Download Clang + #if: steps.cache-clang-installation.outputs.cache-hit != 'true' + run: (New-Object System.Net.WebClient).DownloadFile("https://releases.llvm.org/9.0.0/LLVM-9.0.0-win64.exe", "$(($pwd).path)\LLVM-9.0.0-win64.exe") + shell: pwsh + - name: Install Clang + #if: steps.cache-clang-installation.outputs.cache-hit != 'true' + run: Start-Process .\LLVM-9.0.0-win64.exe -ArgumentList @("/S", "/NCRC", "/D=$(($pwd).path)\clang") -Wait + shell: pwsh + # - name: Test Clang version + # run: .\clang\bin\clang.exe --version + # shell: pwsh + # - name: Find "clang.dll" + # run: ls -r -ea silentlycontinue -fo -inc "libclang.dll" | % { $_.fullname } + # shell: pwsh + + # - name: Download rustup-init + # run: (New-Object System.Net.WebClient).DownloadFile("https://win.rustup.rs/", "$(($pwd).path)\rustup-init.exe") + # shell: pwsh + # - name: Install Rust + # run: .\rustup-init.exe -y --quiet + # shell: pwsh + # - name: Test Rust version + # run: $env:Path = "$env:USERPROFILE\.cargo\bin;$env:Path"; rustc.exe --version + # shell: pwsh + + - name: Update Rust + run: rustup update + shell: pwsh + + # The following steps have to be run in CMD in order to get the environment variables from the .bat scripts + + - name: Build (--features "dss") + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + call .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + set LIBCLANG_PATH=%cd%\clang\bin + cargo build --features "dss" --verbose + shell: cmd + - name: Run tests (--features "dss") + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + call .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + set LIBCLANG_PATH=%cd%\clang\bin + cargo test --features "dss" --verbose + shell: cmd + + - name: Build (--release --features "all") + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + call .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + set LIBCLANG_PATH=%cd%\clang\bin + cargo build --release --features "all" --verbose + shell: cmd + - name: Run tests (--release --features "all") + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + call .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + set LIBCLANG_PATH=%cd%\clang\bin + cargo test --release --features "all" --verbose + shell: cmd + + + build_windows_mkl2020: + + name: Test on Windows (MKL 2020.1) + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + + # Caching of MKL and clang does not work due to cache size limitations of Github + + # - name: Cache MKL installation + # id: cache-mkl-installation + # uses: actions/cache@v1 + # with: + # path: mkl + # key: ${{ runner.os }}-mkl-installed-w_mkl_2020.0.166 + - name: Download MKL installer + #if: steps.cache-mkl-installation.outputs.cache-hit != 'true' + run: (New-Object System.Net.WebClient).DownloadFile("http://registrationcenter-download.intel.com/akdlm/irc_nas/tec/16543/w_mkl_2020.1.216.exe", "$(($pwd).path)\w_mkl_2020.1.216.exe") + shell: pwsh + - name: Extract MKL installer files + #if: steps.cache-mkl-installation.outputs.cache-hit != 'true' + run: Start-Process .\w_mkl_2020.1.216.exe -ArgumentList @("--silent", "--extract-folder", "$(($pwd).path)\mkl-installer", "--extract-only") -Wait + shell: pwsh + - name: Install MKL + #if: steps.cache-mkl-installation.outputs.cache-hit != 'true' + run: Start-Process .\mkl-installer\install.exe -ArgumentList @("install", "--output=$(($pwd).path)\log.txt", "--eula=accept", "--installdir=$(($pwd).path)\mkl") -Wait + shell: pwsh + + # - name: Cache Clang installation + # id: cache-clang-installation + # uses: actions/cache@v1 + # with: + # path: clang + # key: ${{ runner.os }}-clang-installed-LLVM-9.0.0-win64 + - name: Download Clang + #if: steps.cache-clang-installation.outputs.cache-hit != 'true' + run: (New-Object System.Net.WebClient).DownloadFile("https://releases.llvm.org/9.0.0/LLVM-9.0.0-win64.exe", "$(($pwd).path)\LLVM-9.0.0-win64.exe") + shell: pwsh + - name: Install Clang + #if: steps.cache-clang-installation.outputs.cache-hit != 'true' + run: Start-Process .\LLVM-9.0.0-win64.exe -ArgumentList @("/S", "/NCRC", "/D=$(($pwd).path)\clang") -Wait + shell: pwsh + + - name: Update Rust + run: rustup update + shell: pwsh + + # The following steps have to be run in CMD in order to get the environment variables from the .bat scripts + + - name: Build (--features "dss") + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + call .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + set LIBCLANG_PATH=%cd%\clang\bin + cargo build --features "dss" --verbose + shell: cmd + - name: Run tests (--features "dss") + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + call .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + set LIBCLANG_PATH=%cd%\clang\bin + cargo test --features "dss" --verbose + shell: cmd + + - name: Build (--release --features "all") + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + call .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + set LIBCLANG_PATH=%cd%\clang\bin + cargo build --release --features "all" --verbose + shell: cmd + - name: Run tests (--release --features "all") + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + call .\mkl\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64 + set LIBCLANG_PATH=%cd%\clang\bin + cargo test --release --features "all" --verbose + shell: cmd + diff --git a/extern/mkl-sys/.gitignore b/extern/mkl-sys/.gitignore new file mode 100644 index 0000000..6936990 --- /dev/null +++ b/extern/mkl-sys/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/extern/mkl-sys/Cargo.toml b/extern/mkl-sys/Cargo.toml new file mode 100644 index 0000000..c5df616 --- /dev/null +++ b/extern/mkl-sys/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "mkl-sys" +version = "0.1.0" +authors = ["Andreas Longva "] +edition = "2018" +links = "mkl" +build = "build.rs" + +[features] +# Intel MKL module selection +all = [] +dss = [] +sparse-matrix-checker = [] +extended-eigensolver = [] +inspector-executor = [] + +# Configurations +openmp = [] +ilp64 = [] + +[dependencies] + +[build-dependencies] +bindgen = "0.54" + +# Need opt-level = 2 for `bindgen` to be able to finish in reasonable time (i.e. 30 secs instead of 10 minutes) +[profile.dev.package.bindgen] +opt-level = 2 \ No newline at end of file diff --git a/extern/mkl-sys/LICENSE b/extern/mkl-sys/LICENSE new file mode 100644 index 0000000..b187be3 --- /dev/null +++ b/extern/mkl-sys/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019 Andreas Longva + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/extern/mkl-sys/README.md b/extern/mkl-sys/README.md new file mode 100644 index 0000000..b6f6644 --- /dev/null +++ b/extern/mkl-sys/README.md @@ -0,0 +1,57 @@ +# mkl-sys + +[![Build Status](https://github.com/Andlon/mkl-sys/workflows/Build%20and%20run%20tests/badge.svg)](https://github.com/Andlon/mkl-sys/actions) + +Auto-generated bindings to Intel MKL. Currently only supports Linux and Windows, and not considered stable/ready for production use. Only tested with Intel MKL 2019 and 2020. + +This crate relies on Intel MKL having been installed on the target system, +and that the environment is set up for use with MKL. +The easiest way to make it work is to run the provided `mklvars.sh` setup script that is bundled with MKL. +This sets up the environment for use with MKL. This crate then detects the correct Intel MKL installation +by inspecting the value of the `MKLROOT` environment variable. + +Note that we used to support `pkg-config`, but as of Intel MKL 2020, Intel is shipping broken +configurations. Therefore we instead directly rely on the value of `MKLROOT`. + +## Windows support + +### Compile time requirements +To compiled this create on Windows, the following requirements have to be met: +1. To run `bindgen` a Clang (`libclang`) installation is required. According to the `bindgen` [documentation](https://rust-lang.github.io/rust-bindgen/requirements.html#clang) version 3.9 should suffice. A recent pre-built version of Clang can be downloaded on the [LLVM release page](https://releases.llvm.org/download.html). To ensure that `bindgen` can find Clang, the environment variable `LIBCLANG_PATH` used by `bindgen` has to be set to point to the `bin` folder of the Clang installation. +2. On Windows, Clang uses the MSVC standard library. Therefore, the build process should be started from a Visual Studio or Build Tools command prompt. The command prompt can be started from a start menu shortcut created by the Visual Studio installer or by running a `vcvars` script (e.g. `C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat`) in an open command prompt. An IDE such as Clion with a configured MSVC toolchain should already provide this configuration for targets inside of the IDE. +3. The environment variable `MKLROOT` has to be configured properly to point to the path containing the `bin`, `lib`, `include`, etc. folders of MKL (e.g. `C:\Program Files (x86)\IntelSWTools\compilers_and_libraries\windows\mkl`). This can also be done by running the `mklvars.bat` script in the `bin` folder of MKL. + +A script to build the library and run all tests on Windows might then look like this: +``` +call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" +call "C:\Program Files (x86)\IntelSWTools\compilers_and_libraries\windows\mkl\bin\mklvars.bat intel64" +set LIBCLANG_PATH=C:\Program Files\LLVM\bin +cargo test --release --features "all" +``` + +### Run time requirements +During runtime the corresponding redistributable DLLs of MKL (e.g. located in `C:\Program Files (x86)\IntelSWTools\compilers_and_libraries\windows\redist\intel64_win\mkl`) have to be in `PATH`. + +## Known issues +- `bindgen` does not seem to be able to properly handle many preprocessor macros, such as e.g. `dss_create`. +This appears to be related to [this issue](https://github.com/rust-lang/rust-bindgen/issues/753). +- Generating bindings for the entire MKL library might take a lot of time. To circumvent this, you should use features +to enable binding generation only for the parts of the library that you will need. For example, the `dss` feature +generates bindings for the Direct Sparse Solver (DSS) interface. + +A second approach that alleviates long build times due to `bindgen` is to use the following profile override +in your application's TOML file: + +```toml +[profile.dev.package.bindgen] +opt-level = 2 +``` + +This ensures that bindgen is compiled with optimizations on, significantly improving its runtime when +invoked by the build script in `mkl-sys`. + +## License +Intel MKL is provided by Intel and licensed separately. + +This crate is licensed under the MIT license. See `LICENSE` for details. + diff --git a/extern/mkl-sys/build.rs b/extern/mkl-sys/build.rs new file mode 100644 index 0000000..6ccc9f2 --- /dev/null +++ b/extern/mkl-sys/build.rs @@ -0,0 +1,320 @@ +use bindgen::callbacks::{IntKind, ParseCallbacks}; +use bindgen::EnumVariation; +use std::env; +use std::path::PathBuf; + +/// Paths required for linking to MKL from MKLROOT folder +struct MklDirectories { + lib_dir: String, + omp_lib_dir: String, + include_dir: String, +} + +impl MklDirectories { + /// Constructs paths required for linking MKL from the specified root folder. Checks if paths exist. + fn try_new(mkl_root: &str) -> Result { + let os = if cfg!(target_os = "windows") { + "win" + } else if cfg!(target_os = "linux") { + "lin" + } else { + return Err("Target OS not supported".into()); + }; + + let arch = if cfg!(target_arch = "x86_64") { + "64" + } else { + return Err("Target architecture not supported".into()); + }; + + let mkl_root: String = mkl_root.into(); + let prefix: String = mkl_root.clone(); + let exec_prefix: String = prefix.clone(); + let lib_dir = format!( + "{exec_prefix}/lib/intel{arch}_{os}", + exec_prefix = exec_prefix, + arch = arch, + os = os + ); + let omp_lib_dir = format!( + "{exec_prefix}/../compiler/lib/intel{arch}_{os}", + exec_prefix = exec_prefix, + arch = arch, + os = os + ); + let include_dir = format!("{prefix}/include", prefix = prefix); + + let mkl_root_path = PathBuf::from(mkl_root); + let lib_dir_path = PathBuf::from(lib_dir); + let omp_lib_dir_path = PathBuf::from(omp_lib_dir); + let include_dir_path = PathBuf::from(include_dir); + + let mkl_root_str = mkl_root_path + .to_str() + .ok_or("Unable to convert 'mkl_root' to string")?; + let lib_dir_str = lib_dir_path + .to_str() + .ok_or("Unable to convert 'lib_dir_path' to string")?; + let omp_lib_dir_str = omp_lib_dir_path + .to_str() + .ok_or("Unable to convert 'omp_lib_dir_path' to string")?; + let include_dir_str = include_dir_path + .to_str() + .ok_or("Unable to convert 'include_dir_path' to string")?; + + // Check if paths exist + + if !mkl_root_path.exists() { + println!( + "cargo:warning=The 'mkl_root' folder with path '{}' does not exist.", + mkl_root_str + ); + } + + if !lib_dir_path.exists() { + println!( + "cargo:warning=The 'lib_dir_path' folder with path '{}' does not exist.", + lib_dir_str + ); + } + + if cfg!(feature = "openmp") && !omp_lib_dir_path.exists() { + println!( + "cargo:warning=The 'omp_lib_dir_path' folder with path '{}' does not exist.", + omp_lib_dir_str + ); + } + + if !include_dir_path.exists() { + println!( + "cargo:warning=The 'include_dir_path' folder with path '{}' does not exist.", + include_dir_str + ); + } + + Ok(MklDirectories { + lib_dir: lib_dir_str.into(), + omp_lib_dir: omp_lib_dir_str.into(), + include_dir: include_dir_str.into(), + }) + } +} + +fn get_lib_dirs(mkl_dirs: &MklDirectories) -> Vec { + if cfg!(feature = "openmp") { + vec![mkl_dirs.lib_dir.clone(), mkl_dirs.omp_lib_dir.clone()] + } else { + vec![mkl_dirs.lib_dir.clone()] + } +} + +fn get_link_libs_windows() -> Vec { + // Note: The order of the libraries is very important + let mut libs = Vec::new(); + + if cfg!(feature = "ilp64") { + libs.push("mkl_intel_ilp64_dll"); + } else { + libs.push("mkl_intel_lp64_dll"); + }; + + if cfg!(feature = "openmp") { + libs.push("mkl_intel_thread_dll"); + } else { + libs.push("mkl_sequential_dll"); + }; + + libs.push("mkl_core_dll"); + + if cfg!(feature = "openmp") { + libs.push("libiomp5md"); + } + + libs.into_iter().map(|s| s.into()).collect() +} + +fn get_link_libs_linux() -> Vec { + // Note: The order of the libraries is very important + let mut libs = Vec::new(); + + if cfg!(feature = "ilp64") { + libs.push("mkl_intel_ilp64"); + } else { + libs.push("mkl_intel_lp64"); + }; + + if cfg!(feature = "openmp") { + libs.push("mkl_intel_thread"); + } else { + libs.push("mkl_sequential"); + }; + + libs.push("mkl_core"); + + if cfg!(feature = "openmp") { + libs.push("iomp5"); + } + libs.extend(vec!["pthread", "m", "dl"]); + + libs.into_iter().map(|s| s.into()).collect() +} + +fn get_link_libs() -> Vec { + if cfg!(target_os = "windows") { + get_link_libs_windows() + } else if cfg!(target_os = "linux") { + get_link_libs_linux() + } else { + panic!("Target OS not supported"); + } +} + +fn get_cflags_windows(mkl_dirs: &MklDirectories) -> Vec { + let mut cflags = Vec::new(); + + if cfg!(feature = "ilp64") { + cflags.push("-DMKL_ILP64".into()); + } + + cflags.push("--include-directory".into()); + cflags.push(format!("{}", mkl_dirs.include_dir)); + cflags +} + +fn get_cflags_linux(mkl_dirs: &MklDirectories) -> Vec { + let mut cflags = Vec::new(); + + if cfg!(feature = "ilp64") { + cflags.push("-DMKL_ILP64".into()); + } + + cflags.push("-I".into()); + cflags.push(format!("{}", mkl_dirs.include_dir)); + cflags +} + +fn get_cflags(mkl_dirs: &MklDirectories) -> Vec { + if cfg!(target_os = "windows") { + get_cflags_windows(mkl_dirs) + } else if cfg!(target_os = "linux") { + get_cflags_linux(mkl_dirs) + } else { + panic!("Target OS not supported"); + } +} + +#[derive(Debug)] +pub struct Callbacks; + +impl ParseCallbacks for Callbacks { + fn int_macro(&self, name: &str, _value: i64) -> Option { + // This forces all MKL constants to be signed. Otherwise `bindgen` might + // give different types to different constants, which is inconvenient. + // MKL expects these constants to be compatible with MKL_INT. + if &name[..4] == "MKL_" { + // Important: this should be the same as MKL_INT + if cfg!(feature = "ilp64") { + Some(IntKind::I64) + } else { + Some(IntKind::I32) + } + } else { + None + } + } +} + +fn main() { + if cfg!(not(any( + feature = "all", + feature = "dss", + feature = "sparse-matrix-checker", + feature = "extended-eigensolver", + feature = "inspector-executor" + ))) { + panic!( + "No MKL modules selected. +To use this library, please select the features corresponding \ +to MKL modules that you would like to use, or enable the `all` feature if you would \ +like to generate symbols for all modules." + ); + } + + // Link with the proper MKL libraries and simultaneously set up arguments for bindgen. + // Otherwise we don't get e.g. the correct MKL preprocessor definitions). + let clang_args = { + let mklroot = match env::var("MKLROOT") { + Ok(mklroot) => mklroot, + Err(_) => panic!( +"Environment variable 'MKLROOT' is not defined. Remember to run the mklvars script bundled +with MKL in order to set up the required environment variables."), + }; + + let mkl_dirs = MklDirectories::try_new(&mklroot).unwrap(); + + for lib_dir in get_lib_dirs(&mkl_dirs) { + println!("cargo:rustc-link-search=native={}", lib_dir); + } + + for lib in get_link_libs() { + println!("cargo:rustc-link-lib={}", lib); + } + + let args = get_cflags(&mkl_dirs); + args + }; + + #[allow(unused_mut)] + let mut builder = bindgen::Builder::default() + .header("wrapper.h") + .parse_callbacks(Box::new(Callbacks)) + .default_enum_style(EnumVariation::ModuleConsts) + .impl_debug(true) + .derive_debug(true) + .clang_args(clang_args); + + // If only part of MKL is needed, we use features to construct whitelists of + // the needed functionality. These can be overridden with the "all" feature, which + // avoids whitelisting and instead encompasses everything. + #[cfg(not(feature = "all"))] + { + #[cfg(feature = "dss")] + { + let dss_regex = "(dss_.*)|(DSS_.*)|(MKL_DSS.*)"; + builder = builder + .whitelist_function(dss_regex) + .whitelist_type(dss_regex) + .whitelist_var(dss_regex); + } + + #[cfg(feature = "sparse-matrix-checker")] + { + builder = builder + .whitelist_function("sparse_matrix_checker*") + .whitelist_function("sparse_matrix_checker_init*"); + } + + #[cfg(feature = "extended-eigensolver")] + { + builder = builder + .whitelist_function(".*feast.*") + .whitelist_function("mkl_sparse_ee_init") + .whitelist_function("mkl_sparse_._svd") + .whitelist_function("mkl_sparse_._ev") + .whitelist_function("mkl_sparse_._gv"); + } + + #[cfg(feature = "inspector-executor")] + { + builder = builder.whitelist_function("mkl_sparse_.*"); + } + } + + let bindings = builder.generate().expect("Unable to generate bindings"); + + // Write the bindings to the $OUT_DIR/bindings.rs file. + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/extern/mkl-sys/src/lib.rs b/extern/mkl-sys/src/lib.rs new file mode 100644 index 0000000..788bf6d --- /dev/null +++ b/extern/mkl-sys/src/lib.rs @@ -0,0 +1,60 @@ +/*! + +# mkl-sys + +Auto-generated bindings to Intel MKL. + +Currently only tested on Linux, and should be **considered experimental and unstable** +(in an API sense). + +This crate relies on Intel MKL having been installed on the target system, +and that the environment is set up for use with MKL. +It uses `pkg-config` to determine library paths. The easiest way to make it work is to run the provided +`mklvars.sh` setup script that is bundled with MKL. + +The library can generate bindings for only selected modules of MKL, or for the entire library. +By default, no modules are selected, and compilation will fail with an error message. To use +this library, enable the features corresponding to the desired MKL modules, or enable the +"all" feature if you want to generate code for all of MKL. At the moment, the currently available +features corresponding to MKL modules are: + +- `all`: Create bindings for all modules. +- `dss`: The Direct Sparse Solver (DSS) interface. +- `sparse-matrix-checker`: Routines for checking validity of sparse matrix storage. +- `extended-eigensolver`: Routines for the Extended Eigensolver functionality. +- `inspector-executor`: The Inspector-Executor API for sparse matrices. + +It is strongly recommended to only enable the modules that you need, otherwise the effects +on compilation time may be severe. See "Known issues" below. + +By default, the sequential version of MKL is used. To enable OpenMP support, enable the +`openmp` feature. + +By default, 32-bit integers are used for indexing. This corresponds to the `lp64` configuration +of MKL. To use 64-bit integers with the `ilp64` configuration, enable the `ilp64` feature. + +Please refer to the Intel MKL documentation for how to use the functions exposed by this crate. + +## Contributions +Contributions are very welcome. I am generally only adding features to this library as I need them. +If you require something that is not yet available, please file an issue on GitHub +and consider contributing the changes yourself through a pull request. + +## Known issues +- `bindgen` does not handle many preprocessor macros used by MKL, such as e.g. `dss_create`. +It also does not generate type aliases for #define-based type aliases, such as e.g. `MKL_INT`. +Some of these types are manually added to this library, but they do not appear in the +function arguments. +- Generating bindings for the entire MKL library takes a lot of time. This is a significant issue for debug +builds, as we currently have no way of forcing optimizations for bindgen when dependent projects are +built without optimizations. To circumvent this, you should use features to enable binding generation +only for the parts of the library that you will need. For example, the `dss` feature generates bindings for the +Direct Sparse Solver (DSS) interface. + +*/ + +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); \ No newline at end of file diff --git a/extern/mkl-sys/tests/basic.rs b/extern/mkl-sys/tests/basic.rs new file mode 100644 index 0000000..995dc9b --- /dev/null +++ b/extern/mkl-sys/tests/basic.rs @@ -0,0 +1,27 @@ +use mkl_sys::{ + _MKL_DSS_HANDLE_t, dss_create_, dss_delete_, MKL_DSS_DEFAULTS, MKL_DSS_ZERO_BASED_INDEXING, +}; +use std::ptr::null_mut; + +#[test] +/// Calls some arbitrary MKL functions to ensure that linking and running an executable works +fn does_link_and_run() { + let create_opts = MKL_DSS_DEFAULTS + MKL_DSS_ZERO_BASED_INDEXING; + let mut handle: _MKL_DSS_HANDLE_t = null_mut(); + unsafe { + let error = dss_create_(&mut handle, &create_opts); + if error != 0 { + panic!("dss_create error: {}", error); + } + } + + let delete_opts = MKL_DSS_DEFAULTS; + unsafe { + let error = dss_delete_(&mut handle, &delete_opts); + if error != 0 { + panic!("dss_delete error: {}", error); + } + } + + assert!(true); +} diff --git a/extern/mkl-sys/wrapper.h b/extern/mkl-sys/wrapper.h new file mode 100644 index 0000000..0911ab1 --- /dev/null +++ b/extern/mkl-sys/wrapper.h @@ -0,0 +1,21 @@ +#include + +// For some types, MKL uses preprocessor macros as a preprocessor alternative to +// typedefs. Unfortunately, this makes it just about impossible for `bindgen` +// to understand that it's actually a typedef. To remedy the situation, +// we replace the preprocessor macros with actual, proper typedefs. + +/// Underlying MKL_INT type. This is an intermediate type alias introduced by `mkl-sys`, +/// and should never be directly referenced. +typedef MKL_INT ____MKL_SYS_UNDERLYING_MKL_INT; + +/// Underlying MKL_UINT type. This is an intermediate type alias introduced by `mkl-sys`, +/// and should never be directly referenced. +typedef MKL_UINT ____MKL_SYS_UNDERLYING_MKL_UINT; + +#undef MKL_INT +#undef MKL_UINT +typedef ____MKL_SYS_UNDERLYING_MKL_INT MKL_INT; +typedef ____MKL_SYS_UNDERLYING_MKL_UINT MKL_UINT; + +#include \ No newline at end of file diff --git a/fcm_convergence/Cargo.toml b/fcm_convergence/Cargo.toml new file mode 100644 index 0000000..7e75677 --- /dev/null +++ b/fcm_convergence/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "fcm_convergence" +version = "0.1.0" +authors = ["Andreas Longva "] +edition = "2018" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +structopt = "0.3" +simulation_toolbox = { path = "../simulation_toolbox" } +fenris = { path = "../fenris" } +hamilton2 = { path = "../hamilton2" } +mkl-corrode = { git = "https://github.com/Andlon/mkl-corrode.git", rev="0843a0b46234cd88d7a0e7489720514624207ad9", features = [ "openmp" ] } +serde = { version="1.0", features = [ "derive" ] } +serde_json = "1.0" +rayon = "1.3" +chrono = "0.4" \ No newline at end of file diff --git a/fcm_convergence/src/main.rs b/fcm_convergence/src/main.rs new file mode 100644 index 0000000..a540fc2 --- /dev/null +++ b/fcm_convergence/src/main.rs @@ -0,0 +1,1251 @@ +use std::error::Error; +use std::fs::File; +use std::io::Read; +use std::path::{Path, PathBuf}; + +use fenris::allocators::{ElementConnectivityAllocator, FiniteElementMatrixAllocator, VolumeFiniteElementAllocator}; +use fenris::assembly::{ + apply_homogeneous_dirichlet_bc_csr, apply_homogeneous_dirichlet_bc_rhs, assemble_source_term_into, QuadratureTable, +}; +use fenris::connectivity::{ + CellConnectivity, Connectivity, Hex20Connectivity, Hex27Connectivity, Hex8Connectivity, Tet20Connectivity, +}; +use fenris::element::{map_physical_coordinates, ConnectivityNodalDim, ElementConnectivity, ReferenceFiniteElement}; +use fenris::embedding::{ + embed_mesh_3d, embed_mesh_3d_with_opts, embed_quadrature_3d, embed_quadrature_3d_with_opts, EmbedOptions, + EmbeddedModel3d, EmbeddedModelBuilder, Embedding, QuadratureOptions, +}; +use fenris::geometry::polymesh::PolyMesh3d; +use fenris::geometry::procedural::create_rectangular_uniform_hex_mesh; +use fenris::geometry::vtk::{write_vtk, VtkCellConnectivity}; +use fenris::mesh::{Hex20Mesh, Hex27Mesh, HexMesh, Mesh, Tet20Mesh, Tet4Mesh}; +use fenris::model::{FiniteElementInterpolator, MakeInterpolator, NodalModel, NodalModel3d}; +use fenris::nalgebra::allocator::Allocator; +use fenris::nalgebra::storage::Storage; +use fenris::nalgebra::{ + DVector, DVectorSlice, DVectorSliceMut, DefaultAllocator, DimName, DimNameMul, MatrixMN, Point, Point3, RealField, + Vector3, U10, U3, +}; +use fenris::quadrature::{ + hex_quadrature_strength_11, hex_quadrature_strength_3, hex_quadrature_strength_5, tet_quadrature_strength_10, + tet_quadrature_strength_3, tet_quadrature_strength_5, QuadraturePair3d, +}; +use fenris::rtree::GeometryCollectionAccelerator; +use fenris::solid::materials::{LinearElasticMaterial, YoungPoisson}; +use fenris::solid::{ElasticMaterialModel, ElasticityModel, ElasticityModelParallel}; +use fenris::space::{FiniteElementSpace, GeometricFiniteElementSpace}; +use fenris::util::flatten_vertically; +use fenris::vtkio::model::{Attribute, DataSet}; +use fenris::vtkio::IOBuffer; +use hamilton2::calculus::{DifferentiableVectorFunction, VectorFunction}; +use hamilton2::newton::{newton_line_search, BacktrackingLineSearch, NewtonSettings}; +use mkl_corrode::dss; +use mkl_corrode::dss::Definiteness; +use mkl_corrode::dss::MatrixStructure::Symmetric; +use simulation_toolbox::io::msh::{try_mesh_from_bytes, TryConnectivityFromMshElement, TryVertexFromMshNode}; +use std::ops::Add; +use structopt::StructOpt; + +use rayon::prelude::*; + +use fenris::cg::{ConjugateGradient, LinearOperator, RelativeResidualCriterion}; +use fenris::error::estimate_element_L2_error_squared; +use fenris::geometry::{BoundedGeometry, ConvexPolyhedron, Distance, DistanceQuery}; +use fenris::lp_solvers::GlopSolver; +use fenris::CsrMatrix; +use mkl_corrode::sparse::{CsrMatrixHandle, MatrixDescription, SparseOperation}; +use serde::{Deserialize, Serialize}; +use std::cell::Cell; +use std::collections::BTreeMap; +use std::convert::TryFrom; + +/// Timestamped println +macro_rules! tprintln { + ($($arg:tt)*) => { + { + use chrono::offset::Local; + let print_str = format!($($arg)*); + let now = Local::now(); + println!("[{}] {}", + now.format("%H:%M:%S"), + print_str); + } + } +} + +#[derive(Debug, StructOpt)] +struct CommandlineArgs { + #[structopt(short = "-r", long = "--resolutions", help = "Resolution")] + resolutions: Option>, + + #[structopt(long = "--reference-mesh", help = "Reference mesh")] + reference_mesh: Option, + + #[structopt( + long, + default_value = "data", + parse(from_os_str), + help = "Base directory for output files" + )] + output_dir: PathBuf, + #[structopt( + long, + default_value = "assets", + parse(from_os_str), + help = "Base directory for asset files" + )] + asset_dir: PathBuf, + #[structopt( + long, + parse(from_os_str), + help = "Path for the logfile relative to 'output-dir/scene-name'" + )] + log_file: Option, +} + +fn load_mesh(asset_dir: impl AsRef, filename: &str) -> Result, Box> +where + T: RealField, + D: DimName, + C: Connectivity + TryConnectivityFromMshElement, + Point: TryVertexFromMshNode, + DefaultAllocator: Allocator, +{ + let msh_bytes = { + let file_path = asset_dir.as_ref().join("fcm_convergence/").join(filename); + let mut file = File::open(file_path)?; + let mut bytes = Vec::new(); + file.read_to_end(&mut bytes)?; + bytes + }; + try_mesh_from_bytes(&msh_bytes) +} + +#[allow(dead_code)] +fn bump_function(r: f64) -> f64 { + if r.abs() < 1.0 { + (-(1.0 / (1.0 - r * r))).exp() + } else { + 0.0 + } +} + +fn body_force(x: &Point3) -> Vector3 { + // let x0 = Point3::new(0.0, 1.0, 0.0); + // let d = x - x0; + // let r = (d.x * d.x + d.y * d.y).sqrt(); + // let r = (x - x0).magnitude(); + let scale = 500000.0; + // let magnitude = scale * bump_function(3.0 * r); + // let direction = - Vector3::y(); //(x - x0) / (r + 1e-12); + + // TODO: Temporarily using this simpler body force. Should switch back later! + // - 5000.0 * Vector3::new(-0.0, -9.81, 0.0) + // -5000.0 * Vector3::new(0.0, 10.0 * _x.y, 0.0) + // -5000.0 * Vector3::new(0.0, 10.0 * x.y, 0.0) + + // Note: At one point this configuration seems to have worked reasonably well + // let r = (x - x0).magnitude(); + // let scale = 50000.0; + // let magnitude = scale * bump_function(3.0 * r); + // let direction = -Vector3::y(); //(x - x0) / (r + 1e-12); + + // magnitude * direction + // use std::f64; + // use std::f64::consts::PI; + // let cos = |x| f64::cos(x); + let y = x.y; + let z = x.z; + let x = x.x; + let r = (x * x + z * z).sqrt(); + let magnitude = scale * bump_function(3.0 * r) * y; + let direction = -Vector3::y(); + magnitude * direction +} + +// TODO: Remove u_exact and def_grad_exact once we're fully commited to the new approach +// (i.e. not MMS) +// fn u_exact(x: &Point3) -> Vector3 { +// let cos = |x| f64::cos(x); +// let z = x.z; +// let y = x.y; +// let x = x.x; +// (1.0 - cos(y)) * Vector3::new(0.0, cos(2.0 * PI*x)*cos(2.0 * PI*z) - y, 0.0) +// } +// +// fn deformation_gradient_exact(x: &Point3) -> Matrix3 { +// // Construct individual columns of du/dx (du/dx, du/dy, du/dz) +// // let du_dx = Vector3::new(0.0, 0.0, 0.0); +// // let du_dy = x.y.sin() * Vector3::new(f64::cos(2.0 * PI * x.z), 1.0, 1.0); +// // let du_dz = (1.0 - x.y.cos()) * Vector3::new(-2.0 * PI * f64::sin(2.0 * PI * x.z), 0.0, 0.0); +// +// // let u_jacobian = Matrix3::from_columns(&[du_dx, du_dy, du_dz]); +// +// // TODO: Find exact deformation gradient. The above is old +// +// // Note: We keep this in case we want to experiment with +// // other deformations without having to analytically re-evaluate the derivatives +// let mut point_vec = DVector::zeros(3); +// point_vec.copy_from(&x.coords); +// let mut vector_function = VectorFunctionBuilder::with_dimension(3) +// .with_function(|u, x| u.copy_from(&u_exact(&Point3::from(Vector3::from_column_slice(x.as_slice()))))); +// let u_jacobian = approximate_jacobian(&mut vector_function, &point_vec, &1e-6); +// +// let f = Matrix3::identity() + u_jacobian; +// f +// } + +fn create_dataset_with_displacements(mesh: &Mesh, displacements: &[Vector3]) -> DataSet +where + C: VtkCellConnectivity, +{ + let u_vector = flatten_vertically(&displacements).unwrap_or(DVector::zeros(0)); + let mut dataset = DataSet::from(mesh); + if let DataSet::UnstructuredGrid { ref mut data, .. } = dataset { + let displacement_buffer = IOBuffer::from_slice(u_vector.as_slice()); + let attribute = Attribute::Vectors { + data: displacement_buffer, + }; + data.point.push((format!("displacement"), attribute)); + } else { + panic!("Unexpected data"); + } + dataset +} + +// TODO: Remove +// fn save_exact_solution(mesh: &Tet4Mesh, args: &CommandlineArgs) +// -> Result<(), Box> { +// let displacements: Vec<_> = mesh.vertices() +// .iter() +// .map(u_exact) +// .collect(); +// let dataset = create_dataset_with_displacements(&mesh, &displacements); +// let vtk_output_file = args.output_dir.join("fcm_convergence/").join("hemisphere.vtk"); +// write_vtk(dataset, &vtk_output_file, "FCM convergence")?; +// Ok(()) +// } + +fn dump_embedded_solution( + embedded_mesh: &Tet4Mesh, + interpolator: &FiniteElementInterpolator, + u_solution: &DVector, + args: &CommandlineArgs, + filename: impl AsRef, + vtk_title: &str, +) -> Result<(), Box> { + let embedded_displacements = interpolator.interpolate(u_solution); + let dataset = create_dataset_with_displacements(&embedded_mesh, &embedded_displacements); + let vtk_output_file = args + .output_dir + .join("fcm_convergence/") + .join(filename.as_ref()); + write_vtk(dataset, &vtk_output_file, vtk_title)?; + Ok(()) +} + +fn create_fcm_hex8_model( + bg_mesh: &HexMesh, + embedding: Embedding, +) -> Result, Box> { + let quadrature_opts = QuadratureOptions { + stabilization: None, //Some(StabilizationOptions { + // stabilization_factor: 1e-8, + // stabilization_quadrature: hex_quadrature_strength_3() + // }) + }; + + tprintln!("Construct stiffness quadrature"); + let stiffness_quadrature = embed_quadrature_3d_with_opts( + &bg_mesh, + &embedding, + hex_quadrature_strength_3(), + tet_quadrature_strength_3(), + &quadrature_opts, + )?; + tprintln!("Simplifying stiffness quadrature..."); + // TODO: Use simplification in final sim + let stiffness_quadrature = stiffness_quadrature.simplified(3, &GlopSolver::new())?; + tprintln!("Finished stiffness quadrature simplification."); + let elliptic_quadrature = stiffness_quadrature.clone(); + let fe_model = EmbeddedModelBuilder::from_embedding(&bg_mesh, embedding) + .stiffness_quadrature(stiffness_quadrature) + .elliptic_quadrature(elliptic_quadrature) + .build(); + Ok(fe_model) +} + +fn create_fcm_hex20_model( + bg_mesh: &HexMesh, + embedding: Embedding, +) -> Result, Box> { + let quadrature_opts = QuadratureOptions { + stabilization: None, //Some(StabilizationOptions { + // stabilization_factor: 1e-8, + // stabilization_quadrature: hex_quadrature_strength_3() + // }) + }; + + let bg_mesh = Hex20Mesh::from(bg_mesh); + + tprintln!("Construct stiffness quadrature"); + let stiffness_quadrature = embed_quadrature_3d_with_opts( + &bg_mesh, + &embedding, + hex_quadrature_strength_5(), + tet_quadrature_strength_5(), + &quadrature_opts, + )?; + tprintln!("Simplifying stiffness quadrature..."); + // TODO: Use simplification in final sim + let stiffness_quadrature = stiffness_quadrature.simplified(5, &GlopSolver::new())?; + tprintln!("Finished stiffness quadrature simplification."); + let elliptic_quadrature = stiffness_quadrature.clone(); + let fe_model = EmbeddedModelBuilder::from_embedding(&bg_mesh, embedding) + .stiffness_quadrature(stiffness_quadrature) + .elliptic_quadrature(elliptic_quadrature) + .build(); + Ok(fe_model) +} + +fn create_fcm_hex27_model( + bg_mesh: &HexMesh, + embedding: Embedding, +) -> Result, Box> { + let quadrature_opts = QuadratureOptions { + stabilization: None, //Some(StabilizationOptions { + // stabilization_factor: 1e-8, + // stabilization_quadrature: hex_quadrature_strength_3() + // }) + }; + + let bg_mesh = Hex27Mesh::from(bg_mesh); + + tprintln!("Construct stiffness quadrature"); + let stiffness_quadrature = embed_quadrature_3d_with_opts( + &bg_mesh, + &embedding, + hex_quadrature_strength_5(), + tet_quadrature_strength_5(), + &quadrature_opts, + )?; + tprintln!("Simplifying stiffness quadrature..."); + let stiffness_quadrature = stiffness_quadrature; //.simplified(5, &GlopSolver::new())?; + tprintln!("Finished stiffness quadrature simplification."); + let elliptic_quadrature = stiffness_quadrature.clone(); + let fe_model = EmbeddedModelBuilder::from_embedding(&bg_mesh, embedding) + .stiffness_quadrature(stiffness_quadrature) + .elliptic_quadrature(elliptic_quadrature) + .build(); + Ok(fe_model) +} + +#[derive(Serialize, Deserialize)] +pub struct ExperimentResult { + pub resolution: usize, + pub mesh_size: f64, + pub l2_error: f64, +} + +#[derive(Serialize, Deserialize)] +pub struct ExperimentResults { + pub methods: BTreeMap>, +} + +fn main() -> Result<(), Box> { + let args = CommandlineArgs::from_args(); + + // let transform_mesh = |mut mesh: Tet4Mesh| { + // for v in mesh.vertices_mut() { + // let rotation = Rotation3::from_axis_angle(&Unit::new_normalize(Vector3::x()), PI); + // *v = rotation * *v; + // } + // mesh + // }; + + // let mesh: Tet4Mesh = load_mesh(&args.asset_dir, "halfcylinder/halfcylinder_50.msh")?; + let mesh: Tet4Mesh = load_mesh(&args.asset_dir, "hemisphere_50_uniform.msh")?; + // let mesh = transform_mesh(mesh); + // let mesh = { + // let mut mesh = create_rectangular_uniform_hex_mesh(0.5, 2, 1, 2, 16); + // mesh.translate(&Vector3::new(-0.51, 0.0, -0.51)); + // mesh.transform_vertices(|v| *v = Rotation3::from_axis_angle(&Unit::new_normalize(Vector3::y()), PI/4.0) * *v); + // let polymesh = PolyMesh3d::from(&mesh); + // Tet4Mesh::try_from(&polymesh.triangulate()?)? + // }; + let embedded_mesh = PolyMesh3d::from(&mesh); + + tprintln!( + "Loaded embedded mesh with {} elements, {} vertices.", + mesh.connectivity().len(), + mesh.vertices().len() + ); + + let reference = { + // let reference_mesh = mesh.clone(); + // let reference_mesh: Tet4Mesh = load_mesh(&args.asset_dir, "halfcylinder/halfcylinder_50_uniform_refined1.msh")?; + let ref_mesh_filename = args + .reference_mesh + .as_ref() + .cloned() + .unwrap_or("hemisphere_50_uniform_refined1.msh".to_string()); + let reference_mesh: Tet4Mesh = load_mesh(&args.asset_dir, &ref_mesh_filename)?; + // let reference_mesh = transform_mesh(reference_mesh); + // let reference_mesh = { + // let mut mesh = create_rectangular_uniform_hex_mesh(0.93333, 2, 1, 2, 8); + // mesh.translate(&Vector3::new(-1.0, 0.0, -1.0)); + // let polymesh = PolyMesh3d::from(&mesh); + // Tet4Mesh::try_from(&polymesh.triangulate()?)? + // }; + simulate_reference_solution(&reference_mesh, &args)? + }; + + let resolutions = args + .resolutions + .as_ref() + .cloned() + // Pick some sensible (small) defaults + .unwrap_or(vec![1, 2, 4, 8, 16]); + + let mut results = ExperimentResults { + methods: Default::default(), + }; + + for res in resolutions { + tprintln!("=================================="); + tprintln!("Resolution {}...", res); + + let bg_mesh = { + let mut mesh = create_rectangular_uniform_hex_mesh(1.0, 2, 1, 2, res); + mesh.translate(&Vector3::new(-1.0, 0.0, -1.0)); + mesh + }; + let h = 1.0 / (res as f64); + + let mut push_result = |name: &str, l2_error| { + results + .methods + .entry(name.to_string()) + .or_insert_with(|| Vec::new()) + .push(ExperimentResult { + resolution: res, + mesh_size: h, + l2_error, + }); + }; + + tprintln!("Simulating FCM Hex8..."); + { + let l2_error = simulate_fcm_hex8(&bg_mesh, &mesh, &embedded_mesh, res, &args, &reference)?; + push_result("fcm_hex8", l2_error); + } + tprintln!("----------------------------------"); + tprintln!("Simulating embedded FEM Hex8..."); + { + let l2_error = simulate_fem_hex8(&bg_mesh, &mesh, &embedded_mesh, res, &args, &reference)?; + push_result("fem_hex8", l2_error); + } + tprintln!("----------------------------------"); + tprintln!("Simulating FCM Hex20..."); + { + let l2_error = simulate_fcm_hex20(&bg_mesh, &mesh, &embedded_mesh, res, &args, &reference)?; + push_result("fcm_hex20", l2_error); + } + tprintln!("----------------------------------"); + tprintln!("Simulating embedded FEM Hex20..."); + { + let l2_error = simulate_fem_hex20(&bg_mesh, &mesh, &embedded_mesh, res, &args, &reference)?; + push_result("fem_hex20", l2_error); + } + + // tprintln!("Simulating FCM Hex27..."); + // { + // let l2_error = simulate_fcm_hex27(&bg_mesh, &mesh, &embedded_mesh, res, &args, &reference)?; + // push_result("fcm_hex27", l2_error); + // } + + { + let json_filename = args.output_dir.join("fcm_convergence/hex_results.json"); + let mut file = File::create(json_filename)?; + serde_json::to_writer_pretty(&mut file, &results)?; + } + } + + Ok(()) +} + +#[derive(Serialize, Deserialize)] +pub struct ReferenceSolution { + mesh: Tet20Mesh, + displacement: DVector, +} + +fn simulate_reference_solution( + mesh: &Tet4Mesh, + args: &CommandlineArgs, +) -> Result> { + // Use quadratic tets for the reference solution + let mesh = Tet20Mesh::from(mesh); + tprintln!( + "Mesh for reference solution: {} elements, {} nodes.", + mesh.connectivity().len(), + mesh.vertices().len() + ); + let quadrature = tet_quadrature_strength_5(); + let model = NodalModel::from_mesh_and_quadrature(mesh.clone(), quadrature); + + let rhs_cell_quadrature = tet_quadrature_strength_10(); + let rhs_quadrature = |_| &rhs_cell_quadrature; + + let u_inital = DVector::zeros(model.ndof()); + let u_sol = solve_steady_state(&model, &rhs_quadrature, &u_inital, true)?; + + assert_eq!(u_sol.len() % 3, 0); + let displacements: Vec<_> = u_sol + .as_slice() + .chunks_exact(3) + .map(|u_i| Vector3::from_column_slice(u_i)) + .collect(); + let dataset = create_dataset_with_displacements(&mesh, &displacements); + + let filename = "reference_solution.vtk"; + let vtk_output_file = args.output_dir.join("fcm_convergence/").join(filename); + write_vtk(dataset, vtk_output_file, "Reference solution")?; + Ok(ReferenceSolution { + mesh, + displacement: u_sol, + }) +} + +fn construct_bg_mesh_from_embedding(initial_bg_mesh: &HexMesh, embedding: &Embedding) -> HexMesh { + let keep_cells = { + let mut cells = embedding.interface_cells.clone(); + cells.extend(&embedding.interior_cells); + cells.sort_unstable(); + cells + }; + initial_bg_mesh.keep_cells(&keep_cells) +} + +#[allow(unused)] +fn simulate_fem_hex8( + bg_mesh: &HexMesh, + embedded_tet_mesh: &Tet4Mesh, + embedded_mesh: &PolyMesh3d, + res: usize, + args: &CommandlineArgs, + reference: &ReferenceSolution, +) -> Result> { + // Embed the mesh in order to determine the "real" background mesh (i.e. cutting off exterior + // cells). + tprintln!("Embedding mesh..."); + let embedding = embed_mesh_3d(&bg_mesh, &embedded_mesh); + tprintln!( + "Number of background cells: {}", + embedding.interface_cells.len() + embedding.interior_cells.len() + ); + + let mesh = construct_bg_mesh_from_embedding(&bg_mesh, &embedding); + + let quadrature = hex_quadrature_strength_3(); + let fe_model = NodalModel3d::from_mesh_and_quadrature(mesh.clone(), quadrature); + + let rhs_cell_quadrature = hex_quadrature_strength_11(); + let rhs_quadrature = |_| &rhs_cell_quadrature; + + let u_inital = DVector::zeros(fe_model.ndof()); + tprintln!("Solve..."); + let u_sol = solve_steady_state(&fe_model, &rhs_quadrature, &u_inital, false)?; + + tprintln!("Create interpolator..."); + let accelerator = GeometryCollectionAccelerator::new(&fe_model); + let interpolator = accelerator.make_interpolator(&embedded_tet_mesh.vertices())?; + + tprintln!("Estimate L2 error..."); + let bg_poly_mesh = PolyMesh3d::from(&mesh); + let l2_error = estimate_l2_error( + "fem_hex8", + res, + args, + reference, + &bg_poly_mesh, + &accelerator, + DVectorSlice::from(&u_sol), + )?; + tprintln!("L2 error: {:3.3e}", l2_error); + + dump_embedded_solution( + &embedded_tet_mesh, + &interpolator, + &u_sol, + &args, + format!("hemisphere_solved_fem_hex8_{}.vtk", res), + "FEM solved Hex8", + )?; + + Ok(l2_error) +} + +#[allow(unused)] +fn simulate_fem_hex20( + bg_mesh: &HexMesh, + embedded_tet_mesh: &Tet4Mesh, + embedded_mesh: &PolyMesh3d, + res: usize, + args: &CommandlineArgs, + reference: &ReferenceSolution, +) -> Result> { + // Embed the mesh in order to determine the "real" background mesh (i.e. cutting off exterior + // cells). + tprintln!("Embedding mesh..."); + let embedding = embed_mesh_3d(&bg_mesh, &embedded_mesh); + tprintln!( + "Number of background cells: {}", + embedding.interface_cells.len() + embedding.interior_cells.len() + ); + + let hex8_mesh = construct_bg_mesh_from_embedding(&bg_mesh, &embedding); + let mesh = Hex20Mesh::from(&hex8_mesh); + + let quadrature = hex_quadrature_strength_5(); + let fe_model = NodalModel3d::from_mesh_and_quadrature(mesh.clone(), quadrature); + + let rhs_cell_quadrature = hex_quadrature_strength_11(); + let rhs_quadrature = |_| &rhs_cell_quadrature; + + let u_inital = DVector::zeros(fe_model.ndof()); + tprintln!("Solve..."); + let u_sol = solve_steady_state(&fe_model, &rhs_quadrature, &u_inital, false)?; + + tprintln!("Create interpolator..."); + let accelerator = GeometryCollectionAccelerator::new(&fe_model); + let interpolator = accelerator.make_interpolator(&embedded_tet_mesh.vertices())?; + + tprintln!("Estimate L2 error..."); + let bg_poly_mesh = PolyMesh3d::from(&hex8_mesh); + let l2_error = estimate_l2_error( + "fem_hex20", + res, + args, + reference, + &bg_poly_mesh, + &accelerator, + DVectorSlice::from(&u_sol), + )?; + tprintln!("L2 error: {:3.3e}", l2_error); + + dump_embedded_solution( + &embedded_tet_mesh, + &interpolator, + &u_sol, + &args, + format!("hemisphere_solved_fem_hex20_{}.vtk", res), + "FEM solved Hex20", + )?; + + Ok(l2_error) +} + +fn interpolate_displacement<'a, S>( + space: &'a S, + displacements: DVectorSlice, + x_material: &Point3, +) -> Result, Box> +where + S: GeometricFiniteElementSpace<'a, f64> + DistanceQuery<'a, Point3>, + S::Connectivity: ElementConnectivity, + DefaultAllocator: ElementConnectivityAllocator + + Allocator>::NodalDim> + + VolumeFiniteElementAllocator>::NodalDim>, +{ + let element_index = space + .nearest(x_material) + .ok_or_else(|| Box::::from("Failed to find nearest element."))?; + + let connectivity = space.get_connectivity(element_index).unwrap(); + let element = connectivity.element(space.vertices()).unwrap(); + // TODO: It shouldn't be necessary to specify the type here, but there looks as if + // there might be a bug in type inference? + let u_element: MatrixMN = connectivity.element_variables(&displacements); + let xi = map_physical_coordinates(&element, x_material)?; + let phi = element.evaluate_basis(&xi.coords); + let u: Vector3 = u_element * phi.transpose(); + + Ok(u) +} + +fn solve_and_estimate_fcm_error( + method_name: &str, + res: usize, + fe_model: &EmbeddedModel3d, + bg_mesh: &HexMesh, + embedded_tet_mesh: &Tet4Mesh, + embedding: &Embedding, + reference: &ReferenceSolution, + args: &CommandlineArgs, +) -> Result> +where + C: Sync + Connectivity, + C: CellConnectivity, + C::Cell: Sync + BoundedGeometry + Distance>, + C: ElementConnectivity, + U3: DimNameMul, + C::NodalDim: DimNameMul, + DefaultAllocator: ElementConnectivityAllocator + FiniteElementMatrixAllocator, + + // TODO: This shouldn't be necessary, is due to a bug in rustc. Report...? + DefaultAllocator: ElementConnectivityAllocator, +{ + tprintln!("Create interpolator..."); + let accelerator = GeometryCollectionAccelerator::new(fe_model); + let interpolator = accelerator.make_interpolator(&embedded_tet_mesh.vertices())?; + + // Quadrature used for error measurement + // TODO: Increase accuracy? + let rhs_interior_quadrature = hex_quadrature_strength_11(); + tprintln!("Set up error/rhs quadrature"); + let rhs_quadrature = embed_quadrature_3d_with_opts( + bg_mesh, + embedding, + rhs_interior_quadrature.clone(), + tet_quadrature_strength_10(), + &QuadratureOptions::default(), + )?; + + let num_interior = fe_model.interior_connectivity().len(); + let rhs_quadrature_table = |i| -> &QuadraturePair3d { + if i < num_interior { + rhs_quadrature.interior_quadrature() + } else { + rhs_quadrature + .interface_quadratures() + .get(i - num_interior) + .unwrap() + } + }; + + tprintln!("Solve..."); + let u_initial = DVector::zeros(fe_model.ndof()); + let u_sol = solve_steady_state(fe_model, &rhs_quadrature_table, &u_initial, false)?; + + tprintln!("Estimate L2 error..."); + let bg_integration_mesh = construct_bg_mesh_from_embedding(bg_mesh, embedding); + let bg_integration_mesh = PolyMesh3d::from(&bg_integration_mesh); + let l2_error = estimate_l2_error( + method_name, + res, + args, + reference, + &bg_integration_mesh, + &accelerator, + DVectorSlice::from(&u_sol), + )?; + tprintln!("L2 error: {:3.3e}", l2_error); + + dump_embedded_solution( + embedded_tet_mesh, + &interpolator, + &u_sol, + &args, + format!("hemisphere_{}_solved_{}.vtk", method_name, res), + "FCM solved", + )?; + + Ok(l2_error) +} + +fn simulate_fcm_hex8( + bg_mesh: &HexMesh, + mesh: &Tet4Mesh, + embedded_mesh: &PolyMesh3d, + res: usize, + args: &CommandlineArgs, + reference: &ReferenceSolution, +) -> Result> { + tprintln!("Embedding mesh..."); + let embed_opts = EmbedOptions { + upper_volume_threshold: 0.999999, + lower_volume_threshold: 1e-9, + }; + let embedding = embed_mesh_3d_with_opts(&bg_mesh, &embedded_mesh, &embed_opts); + tprintln!( + "Number of background cells: {}", + embedding.interface_cells.len() + embedding.interior_cells.len() + ); + + tprintln!("Creating embedded model"); + let fe_model = create_fcm_hex8_model(&bg_mesh, embedding.clone())?; + + let filename = format!("bgmesh_{}.vtk", res); + let vtk_bg_output_file = args.output_dir.join("fcm_convergence/").join(filename); + write_vtk( + fe_model.background_mesh(), + vtk_bg_output_file, + &format!("Background mesh (res {})", res), + )?; + tprintln!( + "ndofs: {}, interior cells: {}, interface cells: {}", + fe_model.ndof(), + fe_model.interior_connectivity().len(), + fe_model.interface_connectivity().len() + ); + + solve_and_estimate_fcm_error("fcm_hex8", res, &fe_model, &bg_mesh, &mesh, &embedding, reference, args) +} + +fn simulate_fcm_hex20( + bg_mesh: &HexMesh, + mesh: &Tet4Mesh, + embedded_mesh: &PolyMesh3d, + res: usize, + args: &CommandlineArgs, + reference: &ReferenceSolution, +) -> Result> { + tprintln!("Embedding mesh..."); + let embed_opts = EmbedOptions { + upper_volume_threshold: 0.999999, + lower_volume_threshold: 1e-9, + }; + let embedding = embed_mesh_3d_with_opts(&bg_mesh, &embedded_mesh, &embed_opts); + tprintln!( + "Number of background cells: {}", + embedding.interface_cells.len() + embedding.interior_cells.len() + ); + + tprintln!("Creating embedded model"); + let fe_model = create_fcm_hex20_model(&bg_mesh, embedding.clone())?; + + let filename = format!("bgmesh_{}.vtk", res); + let vtk_bg_output_file = args.output_dir.join("fcm_convergence/").join(filename); + write_vtk( + fe_model.background_mesh(), + vtk_bg_output_file, + &format!("Background mesh (res {})", res), + )?; + + tprintln!( + "ndofs: {}, interior cells: {}, interface cells: {}", + fe_model.ndof(), + fe_model.interior_connectivity().len(), + fe_model.interface_connectivity().len() + ); + + solve_and_estimate_fcm_error( + "fcm_hex20", + res, + &fe_model, + &bg_mesh, + &mesh, + &embedding, + reference, + args, + ) +} + +#[allow(dead_code)] +fn simulate_fcm_hex27( + bg_mesh: &HexMesh, + mesh: &Tet4Mesh, + embedded_mesh: &PolyMesh3d, + res: usize, + args: &CommandlineArgs, + reference: &ReferenceSolution, +) -> Result> { + tprintln!("Embedding mesh..."); + let embedding = embed_mesh_3d(&bg_mesh, &embedded_mesh); + tprintln!( + "Number of background cells: {}", + embedding.interface_cells.len() + embedding.interior_cells.len() + ); + + tprintln!("Creating embedded model"); + let fe_model = create_fcm_hex27_model(&bg_mesh, embedding.clone())?; + + let filename = format!("bgmesh_{}.vtk", res); + let vtk_bg_output_file = args.output_dir.join("fcm_convergence/").join(filename); + write_vtk( + fe_model.background_mesh(), + vtk_bg_output_file, + &format!("Background mesh (res {})", res), + )?; + + tprintln!( + "ndofs: {}, interior cells: {}, interface cells: {}", + fe_model.ndof(), + fe_model.interior_connectivity().len(), + fe_model.interface_connectivity().len() + ); + + solve_and_estimate_fcm_error( + "fcm_hex27", + res, + &fe_model, + &bg_mesh, + &mesh, + &embedding, + reference, + args, + ) +} + +fn estimate_l2_error<'a, S>( + method_name: &str, + res: usize, + args: &CommandlineArgs, + reference: &ReferenceSolution, + approx_poly_mesh: &PolyMesh3d, + approx_space: &'a S, + approx_displacements: DVectorSlice, +) -> Result> +where + S: Sync + GeometricFiniteElementSpace<'a, f64> + DistanceQuery<'a, Point3>, + S::Connectivity: ElementConnectivity, + DefaultAllocator: ElementConnectivityAllocator + + Allocator> + + VolumeFiniteElementAllocator>::NodalDim> + // TODO: This seems to be a rustc bug. Obviously the below bound is satisfied even without + // requiring it, since all the types are concrete. However, rust will confuse the types + // with the ones belonging to S otherwise. Probably some normalization issue? + // Maybe report this once we have more time + + VolumeFiniteElementAllocator + + ElementConnectivityAllocator, +{ + let options = EmbedOptions { + // Ensure that cells don't get labeled "interior". + upper_volume_threshold: 10000.0, + lower_volume_threshold: 0.0, + }; + let embedding = embed_mesh_3d_with_opts(&reference.mesh, approx_poly_mesh, &options); + assert_eq!(embedding.interior_cells.len(), 0); + assert_eq!(embedding.interface_cells.len(), reference.mesh.connectivity().len()); + // + let quadrature = tet_quadrature_strength_10(); + let embedded_quadrature = embed_quadrature_3d(&reference.mesh, &embedding, quadrature.clone(), &quadrature)?; + + let per_element_squared_errors: Vec<_> = reference + .mesh + .connectivity() + .par_iter() + .enumerate() + .map(|(i, conn)| { + let element = conn.element(reference.mesh.vertices()).unwrap(); + let u_element = conn.element_variables(&reference.displacement); + + let u_h = |x: &Point3<_>, _| { + interpolate_displacement(approx_space, approx_displacements, x).expect("Interpolation failed") + }; + + let error_quadrature = &embedded_quadrature.interface_quadratures()[i]; + + let l2_squared = estimate_element_L2_error_squared(&element, u_h, &u_element, &error_quadrature); + l2_squared + }) + .collect(); + + let l2_error_squared = per_element_squared_errors.iter().sum::(); + let per_element_errors: Vec<_> = per_element_squared_errors + .iter() + .zip(reference.mesh.cell_iter()) + .map(|(err, cell)| err.sqrt() / cell.compute_volume()) + .collect(); + + let mut dataset = DataSet::from(&reference.mesh); + if let DataSet::UnstructuredGrid { ref mut data, .. } = dataset { + let l2_errors = IOBuffer::from_slice(per_element_errors.as_slice()); + let attribute = Attribute::Scalars { + num_comp: 1, + lookup_table: None, + data: l2_errors, + }; + + data.cell.push((format!("l2_error"), attribute)); + } else { + panic!("Unexpected data"); + } + + let filename = format!("l2_errors_{}_res_{}.vtk", method_name, res); + let vtk_output_file = args.output_dir.join("fcm_convergence/").join(&filename); + write_vtk(dataset, &vtk_output_file, "L2 errors")?; + + Ok(l2_error_squared.sqrt()) +} + +fn solve_steady_state( + fe_model: &M, + rhs_quadrature_table: impl QuadratureTable, + u_initial: &DVector, + use_iterative: bool, +) -> Result, Box> +where + M: ElasticityModelParallel, + M: FiniteElementSpace, + M::Connectivity: ElementConnectivity, + U3: DimNameMul<>::NodalDim>, + DefaultAllocator: ElementConnectivityAllocator + + Allocator>::NodalDim> + // TODO: Obviously these bounds shouldn't be necessary, but without them + // rustc somehow confuses the generic types + + Allocator + + Allocator, +{ + let dirichlet_nodes: Vec<_> = fe_model + .vertices() + .iter() + .enumerate() + .filter(|(_, v)| v.y < 1e-6) + .map(|(i, _)| i) + .collect(); + + tprintln!("Num Dirichlet nodes: {}", dirichlet_nodes.len()); + + let material = LinearElasticMaterial::from(YoungPoisson { + young: 5e4, + poisson: 0.00, + }); + + let rhs = { + let mut rhs = DVector::zeros(fe_model.ndof()); + assemble_source_term_into( + DVectorSliceMut::from(&mut rhs), + fe_model, + &|x| -> Vector3 { body_force(&Point3::from(*x)) }, + &rhs_quadrature_table, + ); + rhs + }; + + let function = SteadyStateFiniteElementFunction { + model: fe_model, + material_model: &material, + rhs: &rhs, + dirichlet_nodes, + use_iterative, + }; + + // Warmstart with the nodal projection from the analytic solution + let mut u_sol = u_initial.clone(); + // let mut u_sol = DVector::zeros(rhs.len()); + + let mut f = DVector::zeros(u_sol.len()); + let mut dx = f.clone(); + + let settings = NewtonSettings { + max_iterations: None, + tolerance: 1e-9 * rhs.norm(), + }; + + let newton_result = newton_line_search( + function, + &mut u_sol, + &mut f, + &mut dx, + settings, + &mut BacktrackingLineSearch, + ); + + match newton_result { + Ok(iter) => { + tprintln!("Newton iters: {}", iter) + } + Err(err) => { + tprintln!("Newton error: {}", err) + } + }; + + Ok(u_sol) +} + +/// Vector function defining the (discrete) equations for static equilibrium (steady state). +pub struct SteadyStateFiniteElementFunction<'a, M, Material> { + model: &'a M, + material_model: &'a Material, + rhs: &'a DVector, + dirichlet_nodes: Vec, + use_iterative: bool, +} + +impl<'a, M, Material> VectorFunction for SteadyStateFiniteElementFunction<'a, M, Material> +where + M: ElasticityModelParallel, + Material: Sync + ElasticMaterialModel, +{ + fn dimension(&self) -> usize { + self.model.ndof() + } + + fn eval_into(&mut self, f: &mut DVectorSliceMut, x: &DVectorSlice) { + // We solve the non-linear equation + // - f_int(u) - rhs = 0 + // so the left-hand side becomes the vector function F(u) + + // Is this necessary...? + f.fill(0.0); + self.model.assemble_elastic_pseudo_forces_into_par( + DVectorSliceMut::from(&mut *f), + DVectorSlice::from(x), + self.material_model, + ); + *f *= -1.0; + *f -= self.rhs; + apply_homogeneous_dirichlet_bc_rhs(f, &self.dirichlet_nodes, 3); + } +} + +impl<'a, M, Material> DifferentiableVectorFunction for SteadyStateFiniteElementFunction<'a, M, Material> +where + M: ElasticityModelParallel, + Material: Sync + ElasticMaterialModel, +{ + #[allow(non_snake_case)] + fn solve_jacobian_system( + &mut self, + sol: &mut DVectorSliceMut, + x: &DVectorSlice, + rhs: &DVectorSlice, + ) -> Result<(), Box> { + // TODO: Reuse sparsity pattern etc. + tprintln!("Assembling stiffness matrix..."); + let mut A = self + .model + .assemble_stiffness_par(&x.clone_owned(), self.material_model) + .to_csr(Add::add); + apply_homogeneous_dirichlet_bc_csr::<_, U3>(&mut A, &self.dirichlet_nodes); + tprintln!( + "Assembled stiffness matrix: {} nnz, ({} x {})", + A.nnz(), + A.nrows(), + A.ncols() + ); + + if self.use_iterative { + let jacobi_precond_elements: Vec<_> = A.diag_iter().map(|a_ii| a_ii.recip()).collect(); + let p = CsrMatrix::from_diagonal(DVectorSlice::from_slice( + jacobi_precond_elements.as_slice(), + jacobi_precond_elements.len(), + )); + + use mkl_corrode::mkl_sys::MKL_INT; + let convert_to_mkl_int = |indices: &[usize]| { + indices + .iter() + .map(|idx| MKL_INT::try_from(*idx).unwrap()) + .collect::>() + }; + + let p_row_offsets = convert_to_mkl_int(p.row_offsets()); + let p_col_indices = convert_to_mkl_int(p.column_indices()); + let p_mkl = CsrMatrixHandle::from_csr_data( + p.nrows(), + p.ncols(), + &p_row_offsets[..p.nrows()], + &p_row_offsets[1..], + &p_col_indices, + p.values(), + ) + .expect("Sparse matrix construction should never fail"); + p_mkl + .set_mv_hint(SparseOperation::NonTranspose, &MatrixDescription::default(), 2000) + .map_err(|_| Box::::from("MKL error during set_mv_hint"))?; + p_mkl + .optimize() + .map_err(|_| Box::::from("MKL error during optimize"))?; + + let a_row_offsets = convert_to_mkl_int(A.row_offsets()); + let a_col_indices = convert_to_mkl_int(A.column_indices()); + let a_mkl = CsrMatrixHandle::from_csr_data( + A.nrows(), + A.ncols(), + &a_row_offsets[..A.nrows()], + &a_row_offsets[1..], + &a_col_indices, + A.values(), + ) + .expect("Sparse matrix construction should never fail"); + a_mkl + .set_mv_hint(SparseOperation::NonTranspose, &MatrixDescription::default(), 2000) + .map_err(|_| Box::::from("MKL error during set_mv_hint"))?; + a_mkl + .optimize() + .map_err(|_| Box::::from("MKL error during optimize"))?; + p_mkl + .set_mv_hint(SparseOperation::NonTranspose, &MatrixDescription::default(), 2000) + .map_err(|_| Box::::from("MKL error during set_mv_hint"))?; + p_mkl + .optimize() + .map_err(|_| Box::::from("MKL error during optimize"))?; + + let p_mkl_op = MklCsrLinearOperator(&p_mkl); + let a_mkl_op = MklCsrLinearOperator(&a_mkl); + + tprintln!("Solving with CG..."); + let cg_result = ConjugateGradient::new() + .with_operator(DebugOperator::new(a_mkl_op)) + .with_preconditioner(&p_mkl_op) + .with_stopping_criterion(RelativeResidualCriterion::new(1e-9)) + .solve_with_guess(rhs, DVectorSliceMut::from(&mut *sol))?; + + tprintln!("CG iterations: {}", cg_result.num_iterations); + } else { + let A_dss = + dss::SparseMatrix::try_convert_from_csr(A.row_offsets(), A.column_indices(), A.values(), Symmetric)?; + let options = dss::SolverOptions::default().parallel_reorder(true); + tprintln!("Factoring system..."); + let mut solver = dss::Solver::try_factor_with_opts(&A_dss, Definiteness::PositiveDefinite, &options)?; + tprintln!("Done factoring."); + let solution = solver.solve(rhs.data.as_slice())?; + sol.copy_from_slice(&solution); + } + Ok(()) + } +} + +struct DebugOperator { + op: Op, + iters: Cell, +} + +impl DebugOperator { + pub fn new(op: Op) -> Self { + Self { + op, + iters: Cell::new(0), + } + } +} + +impl LinearOperator for DebugOperator +where + Op: LinearOperator, +{ + fn apply(&self, y: DVectorSliceMut, x: DVectorSlice) -> Result<(), Box> { + self.op.apply(y, x)?; + self.iters.replace(self.iters.get() + 1); + if self.iters.get() % 100 == 0 { + tprintln!("Finished {} iterations...", self.iters.get()); + } + Ok(()) + } +} + +struct MklCsrLinearOperator<'a>(&'a CsrMatrixHandle<'a, f64>); + +impl<'a> LinearOperator for MklCsrLinearOperator<'a> { + fn apply(&self, mut y: DVectorSliceMut, x: DVectorSlice) -> Result<(), Box> { + assert_eq!(y.len(), x.len()); + assert_eq!(y.len(), self.0.rows()); + assert_eq!(x.len(), self.0.cols()); + + let description = MatrixDescription::default(); + mkl_corrode::sparse::spmv_csr( + SparseOperation::NonTranspose, + 1.0, + self.0, + &description, + x.as_slice(), + 0.0, + y.as_mut_slice(), + ) + .map_err(|_| Box::::from("MKL error during sparse spmv"))?; + Ok(()) + } +} diff --git a/fenris/Cargo.toml b/fenris/Cargo.toml new file mode 100644 index 0000000..b2035e0 --- /dev/null +++ b/fenris/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "fenris" +version = "0.1.0" +authors = ["Andreas Longva "] +edition = "2018" +publish = false + +[features] +default = [ "proptest" ] + +[dependencies] +nalgebra = { version = "0.21", features = [ "serde-serialize" ] } +alga = { version = "0.9", default-features = false } +vtkio = "0.3" +num = "0.2" +numeric_literals = "0.2.0" +itertools = "0.9" +ordered-float = "1.0" +proptest = { version = "0.9", optional = true } +rstar = "0.9.1" +rayon = "1.3" +lp-bfp = { path = "../lp-bfp" } +nested-vec = { path="../nested-vec" } +hamilton2 = { path="../hamilton2" } +# TODO: Make serde optional +serde = { version="1.0", features = [ "derive" ] } +arrayvec = "0.5.1" +log = "0.4" +paradis = { path = "../paradis" } +rustc-hash = "1.1.0" +thread_local = "1.*" +delegate = "0.6.1" + +[dev-dependencies] +proptest = "0.9" +prettytable-rs = "^0.8" +matrixcompare = "0.3.0" +mkl-corrode = { git = "https://github.com/Andlon/mkl-corrode.git", rev="0843a0b46234cd88d7a0e7489720514624207ad9" } +paste = "0.1.7" +criterion = "0.3.2" + +[[bench]] +name = "assembly" +harness = false diff --git a/fenris/benches/assembly.rs b/fenris/benches/assembly.rs new file mode 100644 index 0000000..2352715 --- /dev/null +++ b/fenris/benches/assembly.rs @@ -0,0 +1,234 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use fenris::geometry::polymesh::PolyMesh3d; +use fenris::geometry::procedural::create_rectangular_uniform_hex_mesh; +use fenris::mesh::{HexMesh, Tet10Mesh, Tet4Mesh}; +use fenris::model::NodalModel; +use fenris::quadrature::{tet_quadrature_strength_1, tet_quadrature_strength_5}; +use fenris::solid::materials::{StableNeoHookeanMaterial, YoungPoisson}; +use fenris::solid::{ElasticMaterialModel, ElasticityModel, ElasticityModelParallel}; +use nalgebra::{DMatrix, DMatrixSliceMut, DVector, Dynamic, Matrix3, MatrixMN, MatrixSliceMN, U3}; +use std::convert::TryFrom; +use std::ops::Add; + +fn test_mesh() -> HexMesh { + create_rectangular_uniform_hex_mesh(1.0, 1, 1, 1, 4) +} + +pub fn stable_neo_hookean_contraction_3d(c: &mut Criterion) { + let material = StableNeoHookeanMaterial::from(YoungPoisson { + young: 1e6, + poisson: 0.45, + }); + let deformation_gradient = Matrix3::new( + 52.85734952, + -19.73633697, + -30.87845429, + 26.67331831, + -10.35380109, + -16.15435165, + 58.20810674, + -20.01345825, + -31.60294891, + ); + + let contraction_vectors = black_box(MatrixMN::::from_row_slice(&[ + 0.79823853, 0.53879483, 0.6145651, 0.56647738, 0.80380162, 0.81328391, 0.92302888, 0.81700116, 0.40729593, + 0.36585753, 0.42701343, 0.69061995, 0.90149585, 0.17902489, 0.29973298, 0.8654594, 0.39307017, 0.33597961, + 0.89614737, 0.03698405, 0.9097741, 0.90695223, 0.05189938, 0.49869605, 0.32052228, 0.44186043, 0.32517814, + 0.16204256, 0.14232612, 0.707076, + ])); + + let output_dim = 3 * contraction_vectors.ncols(); + let mut output = DMatrix::zeros(output_dim, output_dim); + c.bench_function("stable_neo_hookean_contraction_3d", |b| { + b.iter(|| { + material.contract_multiple_stress_tensors_into( + &mut DMatrixSliceMut::from(&mut output), + &black_box(deformation_gradient.clone()), + &MatrixSliceMN::from(&contraction_vectors), + ) + }) + }); +} + +pub fn stable_neo_hookean_assemble_tet10_mesh(c: &mut Criterion) { + let mesh = test_mesh(); + let mesh = Tet4Mesh::try_from(&PolyMesh3d::from(&mesh).triangulate().unwrap()).unwrap(); + let mesh = Tet10Mesh::from(&mesh); + let quadrature = tet_quadrature_strength_5(); + + let model = NodalModel::from_mesh_and_quadrature(mesh, quadrature); + + let u = DVector::zeros(model.ndof()); + let material = StableNeoHookeanMaterial::from(YoungPoisson { + young: 1e6, + poisson: 0.45, + }); + + c.bench_function("stable_neo_hookean_assemble_tet10_mesh", |b| { + b.iter(|| { + model.assemble_stiffness(&u, &material); + }) + }); + + c.bench_function("stable_neo_hookean_assemble_tet10_mesh_to_csr", |b| { + b.iter(|| { + model.assemble_stiffness(&u, &material).to_csr(Add::add); + }) + }); +} + +pub fn stable_neo_hookean_assemble_tet10_mesh_into_csr(c: &mut Criterion) { + let mesh = test_mesh(); + let mesh = Tet4Mesh::try_from(&PolyMesh3d::from(&mesh).triangulate().unwrap()).unwrap(); + let mesh = Tet10Mesh::from(&mesh); + let quadrature = tet_quadrature_strength_5(); + + let model = NodalModel::from_mesh_and_quadrature(mesh, quadrature); + + let u = DVector::zeros(model.ndof()); + let material = StableNeoHookeanMaterial::from(YoungPoisson { + young: 1e6, + poisson: 0.45, + }); + + let mut csr = model.assemble_stiffness(&u, &material).to_csr(Add::add); + + c.bench_function("stable_neo_hookean_assemble_tet10_mesh_into_csr", |b| { + b.iter(|| { + model.assemble_stiffness_into(&mut csr, &u, &material); + }) + }); +} +pub fn stable_neo_hookean_assemble_tet4_mesh(c: &mut Criterion) { + let mesh = test_mesh(); + let mesh = Tet4Mesh::try_from(&PolyMesh3d::from(&mesh).triangulate().unwrap()).unwrap(); + let quadrature = tet_quadrature_strength_1(); + + let model = NodalModel::from_mesh_and_quadrature(mesh, quadrature); + + let u = DVector::zeros(model.ndof()); + let material = StableNeoHookeanMaterial::from(YoungPoisson { + young: 1e6, + poisson: 0.45, + }); + + c.bench_function("stable_neo_hookean_assemble_tet4_mesh", |b| { + b.iter(|| { + model.assemble_stiffness(&u, &material); + }) + }); + + c.bench_function("stable_neo_hookean_assemble_tet4_mesh_to_csr", |b| { + b.iter(|| { + model.assemble_stiffness(&u, &material).to_csr(Add::add); + }) + }); +} + +pub fn stable_neo_hookean_assemble_tet4_mesh_into_csr(c: &mut Criterion) { + let mesh = test_mesh(); + let mesh = Tet4Mesh::try_from(&PolyMesh3d::from(&mesh).triangulate().unwrap()).unwrap(); + let quadrature = tet_quadrature_strength_1(); + + let model = NodalModel::from_mesh_and_quadrature(mesh, quadrature); + + let u = DVector::zeros(model.ndof()); + let material = StableNeoHookeanMaterial::from(YoungPoisson { + young: 1e6, + poisson: 0.45, + }); + + let mut csr = model.assemble_stiffness(&u, &material).to_csr(Add::add); + + c.bench_function("stable_neo_hookean_assemble_tet4_mesh_into_csr", |b| { + b.iter(|| { + model.assemble_stiffness_into(&mut csr, &u, &material); + }) + }); +} + +pub fn stable_neo_hookean_assemble_tet10_mesh_parallel(c: &mut Criterion) { + let mesh = test_mesh(); + let mesh = Tet4Mesh::try_from(&PolyMesh3d::from(&mesh).triangulate().unwrap()).unwrap(); + let mesh = Tet10Mesh::from(&mesh); + let quadrature = tet_quadrature_strength_5(); + + let model = NodalModel::from_mesh_and_quadrature(mesh, quadrature); + + let u = DVector::zeros(model.ndof()); + let material = StableNeoHookeanMaterial::from(YoungPoisson { + young: 1e6, + poisson: 0.45, + }); + + c.bench_function("stable_neo_hookean_assemble_tet10_mesh_parallel", |b| { + b.iter(|| { + model.assemble_stiffness_par(&u, &material); + }) + }); + + c.bench_function("stable_neo_hookean_assemble_tet10_mesh_to_csr_parallel", |b| { + b.iter(|| { + model.assemble_stiffness_par(&u, &material).to_csr(Add::add); + }) + }); +} + +pub fn stable_neo_hookean_assemble_tet10_mesh_into_csr_parallel(c: &mut Criterion) { + let mesh = test_mesh(); + let mesh = Tet4Mesh::try_from(&PolyMesh3d::from(&mesh).triangulate().unwrap()).unwrap(); + let mesh = Tet10Mesh::from(&mesh); + let quadrature = tet_quadrature_strength_5(); + + let model = NodalModel::from_mesh_and_quadrature(mesh, quadrature); + + let u = DVector::zeros(model.ndof()); + let material = StableNeoHookeanMaterial::from(YoungPoisson { + young: 1e6, + poisson: 0.45, + }); + + let mut csr = model.assemble_stiffness(&u, &material).to_csr(Add::add); + + c.bench_function("stable_neo_hookean_assemble_tet10_mesh_into_csr_par", |b| { + b.iter(|| { + model.assemble_stiffness_into_par(&mut csr, &u, &material); + }) + }); +} + +pub fn stable_neo_hookean_assemble_tet4_mesh_into_csr_parallel(c: &mut Criterion) { + let mesh = test_mesh(); + let mesh = Tet4Mesh::try_from(&PolyMesh3d::from(&mesh).triangulate().unwrap()).unwrap(); + let quadrature = tet_quadrature_strength_1(); + + let model = NodalModel::from_mesh_and_quadrature(mesh, quadrature); + + let u = DVector::zeros(model.ndof()); + let material = StableNeoHookeanMaterial::from(YoungPoisson { + young: 1e6, + poisson: 0.45, + }); + + let mut csr = model.assemble_stiffness(&u, &material).to_csr(Add::add); + + c.bench_function("stable_neo_hookean_assemble_tet4_mesh_into_csr_par", |b| { + b.iter(|| { + model.assemble_stiffness_into_par(&mut csr, &u, &material); + }) + }); +} + +criterion_group!( + benches, + stable_neo_hookean_contraction_3d, + stable_neo_hookean_assemble_tet4_mesh, + stable_neo_hookean_assemble_tet4_mesh_into_csr, + stable_neo_hookean_assemble_tet4_mesh_into_csr_parallel, + stable_neo_hookean_assemble_tet10_mesh, + stable_neo_hookean_assemble_tet10_mesh_into_csr, + stable_neo_hookean_assemble_tet10_mesh_into_csr_parallel, + stable_neo_hookean_assemble_tet10_mesh_parallel +); +criterion_main!(benches); diff --git a/fenris/examples/embed_mesh_3d.rs b/fenris/examples/embed_mesh_3d.rs new file mode 100644 index 0000000..b421e5f --- /dev/null +++ b/fenris/examples/embed_mesh_3d.rs @@ -0,0 +1,68 @@ +use fenris::embedding::embed_mesh_3d; +use fenris::geometry::polymesh::PolyMesh3d; +use fenris::geometry::procedural::create_rectangular_uniform_hex_mesh; + +use fenris::geometry::vtk::write_vtk; +use nalgebra::{Rotation3, Unit, Vector3}; + +use std::error::Error; +use std::time::Instant; + +fn main() -> Result<(), Box> { + let bg_mesh = create_rectangular_uniform_hex_mesh(2.0, 2, 1, 1, 8); + + let embed_mesh = { + let mut embed_mesh = create_rectangular_uniform_hex_mesh(0.5, 1, 1, 1, 4); + let rotation = Rotation3::from_axis_angle(&Unit::new_normalize(Vector3::new(1.0, 1.0, 1.0)), 0.45); + let t = Vector3::new(0.50, 0.50, 0.50); + embed_mesh.transform_vertices(|v| *v = rotation * v.clone() + t); + PolyMesh3d::from(&embed_mesh) + }; + + println!( + "Embedded mesh: {} cells, {} vertices.", + embed_mesh.num_cells(), + embed_mesh.vertices().len() + ); + println!( + "Background mesh: {} cells, {} vertices.", + bg_mesh.connectivity().len(), + bg_mesh.vertices().len() + ); + + write_vtk(&bg_mesh, "data/embed_mesh_3d/bg_mesh.vtk", "bg mesh")?; + write_vtk(&embed_mesh, "data/embed_mesh_3d/embed_mesh.vtk", "embedded mesh")?; + + println!("Embedding..."); + let now = Instant::now(); + let embedding = embed_mesh_3d(&bg_mesh, &embed_mesh); + let elapsed = now.elapsed().as_secs_f64(); + println!("Completed embedding in {:2.2} seconds.", elapsed); + + let exterior_mesh = bg_mesh.keep_cells(&embedding.exterior_cells); + let interior_mesh = bg_mesh.keep_cells(&embedding.interior_cells); + let interface_mesh = bg_mesh.keep_cells(&embedding.interface_cells); + + let mut keep_cells = embedding.interior_cells.clone(); + keep_cells.extend(embedding.interface_cells.iter().copied()); + keep_cells.sort_unstable(); + let adapted_bg_mesh = bg_mesh.keep_cells(&keep_cells); + println!( + "Adapted bg mesh: {} cells, {} vertices.", + adapted_bg_mesh.connectivity().len(), + adapted_bg_mesh.vertices().len() + ); + + write_vtk(&exterior_mesh, "data/embed_mesh_3d/exterior.vtk", "exterior mesh")?; + write_vtk(&interior_mesh, "data/embed_mesh_3d/interior.vtk", "interior mesh")?; + write_vtk(&interface_mesh, "data/embed_mesh_3d/interface.vtk", "interface mesh")?; + + let aggregate_interface_mesh = PolyMesh3d::concatenate(&embedding.interface_cell_embeddings); + write_vtk( + &aggregate_interface_mesh, + "data/embed_mesh_3d/aggregate_interface.vtk", + "aggregate interface", + )?; + + Ok(()) +} diff --git a/fenris/examples/meshgen.rs b/fenris/examples/meshgen.rs new file mode 100644 index 0000000..e93d985 --- /dev/null +++ b/fenris/examples/meshgen.rs @@ -0,0 +1,60 @@ +use fenris::geometry::procedural::{ + approximate_quad_mesh_for_sdf_2d, approximate_triangle_mesh_for_sdf_2d, voxelize_sdf_2d, +}; +use fenris::geometry::sdf::SdfCircle; +use fenris::geometry::vtk::{ + create_vtk_data_set_from_polygons, create_vtk_data_set_from_quad_mesh, create_vtk_data_set_from_triangle_mesh, + write_vtk, +}; + +use fenris::embedding::embed_mesh_2d; +use nalgebra::Vector2; +use vtkio::Error; + +pub fn main() -> Result<(), Error> { + let voxelize_resolution = 0.23; + let fitted_resolution = 0.1; + let sdf = SdfCircle { + radius: 1.0, + center: Vector2::zeros(), + }; + + let voxelized_mesh = voxelize_sdf_2d(&sdf, voxelize_resolution); + let voxelized_data_set = create_vtk_data_set_from_quad_mesh(&voxelized_mesh); + write_vtk( + voxelized_data_set, + "data/circle_mesh_voxelized.vtk", + "data/voxelized circle", + )?; + + let fitted_quad_mesh = approximate_quad_mesh_for_sdf_2d(&sdf, fitted_resolution); + let fitted_quad_mesh_data_set = create_vtk_data_set_from_quad_mesh(&fitted_quad_mesh); + write_vtk( + fitted_quad_mesh_data_set, + "data/circle_mesh_fitted.vtk", + "data/fitted circle quad mesh", + )?; + + let fitted_triangle_mesh = approximate_triangle_mesh_for_sdf_2d(&sdf, fitted_resolution); + let fitted_triangle_mesh_data_set = create_vtk_data_set_from_triangle_mesh(&fitted_triangle_mesh); + write_vtk( + fitted_triangle_mesh_data_set, + "data/circle_mesh_triangle_fitted.vtk", + "data/fitted circle triangle mesh", + )?; + + let embedding = embed_mesh_2d(&voxelized_mesh, &fitted_triangle_mesh).unwrap(); + let polygons = embedding + .into_iter() + .map(|(_, polygons)| polygons) + .flatten() + .collect::>(); + let embedded_data_set = create_vtk_data_set_from_polygons(&polygons); + write_vtk( + embedded_data_set, + "data/embedded_mesh.vtk", + "data/triangle mesh embedded in quad mesh", + )?; + + Ok(()) +} diff --git a/fenris/examples/poisson.rs b/fenris/examples/poisson.rs new file mode 100644 index 0000000..8f764a8 --- /dev/null +++ b/fenris/examples/poisson.rs @@ -0,0 +1,170 @@ +mod poisson_common; +use poisson_common::*; + +use fenris::assembly::{ + apply_homogeneous_dirichlet_bc_csr, apply_homogeneous_dirichlet_bc_rhs, assemble_generalized_mass, + assemble_generalized_stiffness, UniformQuadratureTable, +}; +use fenris::element::ElementConnectivity; +use fenris::error::estimate_element_L2_error_squared; +use fenris::geometry::procedural::create_rectangular_uniform_quad_mesh_2d; +use fenris::mesh::Mesh2d; +use fenris::quadrature::{quad_quadrature_strength_11, quad_quadrature_strength_5, Quadrature2d}; +use nalgebra::storage::Storage; +use nalgebra::{DVector, DefaultAllocator, DimNameMul, RealField, Scalar, Vector1, Vector2, U1, U2}; +use std::error::Error; + +use fenris::allocators::FiniteElementMatrixAllocator; +use fenris::connectivity::{CellConnectivity, Connectivity}; +use fenris::CsrMatrix; +use itertools::izip; +use mkl_corrode::dss; +use mkl_corrode::dss::Definiteness; +use mkl_corrode::dss::MatrixStructure::Symmetric; +use std::ops::Add; + +pub struct TestProblem +where + T: Scalar, +{ + pub solution: Solution, + pub rhs: Rhs, + pub mesh: Mesh2d, +} + +pub struct PoissonSystem +where + T: Scalar, +{ + pub mass_matrix: CsrMatrix, + pub stiffness_matrix: CsrMatrix, + pub rhs: DVector, +} + +#[allow(non_snake_case)] +pub fn build_poisson_system( + problem: &TestProblem, +) -> Result, Box> +where + T: RealField + mkl_corrode::SupportedScalar, + Solution: Fn(T, T) -> T, + Rhs: Fn(T, T) -> T, + Conn: ElementConnectivity + CellConnectivity, + Conn::FaceConnectivity: Connectivity, + U1: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, +{ + let f = &problem.rhs; + let vertices = problem.mesh.vertices(); + let connectivity = problem.mesh.connectivity(); + + let operator = PoissonEllipticOperator; + let u = DVector::zeros(vertices.len()); + let quadrature = quad_quadrature_strength_5(); + let qtable = UniformQuadratureTable(&quadrature); + + // Assemble system matrix and right hand side + let mut A = assemble_generalized_stiffness(vertices, connectivity, &operator, &u, &qtable).to_csr(Add::add); + let mut M = assemble_generalized_mass::<_, U1, _, _>(vertices, connectivity, T::one(), &qtable).to_csr(Add::add); + let f_nodes = DVector::from_iterator(vertices.len(), vertices.iter().map(|v| f(v.x, v.y))); + let mut rhs = &M * &f_nodes; + + // Apply BC + let boundary_vertices = problem.mesh.find_boundary_vertices(); + apply_homogeneous_dirichlet_bc_csr::<_, U1>(&mut A, &boundary_vertices); + apply_homogeneous_dirichlet_bc_csr::<_, U1>(&mut M, &boundary_vertices); + apply_homogeneous_dirichlet_bc_rhs(&mut rhs, &boundary_vertices, 1); + + Ok(PoissonSystem { + mass_matrix: M, + stiffness_matrix: A, + rhs, + }) +} + +#[allow(non_snake_case)] +pub fn solve_2d_test_problem( + problem: &TestProblem, +) -> Result, Box> +where + T: RealField + mkl_corrode::SupportedScalar, + Solution: Fn(T, T) -> T, + Rhs: Fn(T, T) -> T, + Conn: ElementConnectivity + CellConnectivity, + Conn::FaceConnectivity: Connectivity, + U1: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, +{ + let system = build_poisson_system(problem)?; + + let A = system.stiffness_matrix; + let rhs = system.rhs; + + let A_dss = dss::SparseMatrix::try_convert_from_csr(A.row_offsets(), A.column_indices(), A.values(), Symmetric)?; + let options = dss::SolverOptions::default().parallel_reorder(true); + let mut solver = dss::Solver::try_factor_with_opts(&A_dss, Definiteness::PositiveDefinite, &options)?; + let solution = solver.solve(rhs.data.as_slice()).unwrap(); + + Ok(DVector::from_vec(solution)) +} + +pub fn estimate_l2_error( + mesh: &Mesh2d, + u_h: &DVector, + u_exact: impl Fn(T, T) -> T, + quadrature: impl Quadrature2d, +) -> T +where + T: RealField, + Connectivity: ElementConnectivity + CellConnectivity, + U1: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, +{ + // In the below, we compute the error in the same mesh as was used for computation. + // This should generally be fine for getting an approximate error measure, provided + // that the quadrature is of high order compared to the elements used, + // so that the integration error is much smaller than the discretization error. + let mut l2_error_squared = T::zero(); + let elements = mesh + .connectivity() + .iter() + .map(|conn| conn.element(mesh.vertices()).unwrap()); + for (element, conn) in izip!(elements, mesh.connectivity()) { + let u_h_weights = conn.element_variables(u_h); + l2_error_squared += estimate_element_L2_error_squared( + &element, + |p, _| Vector1::new(u_exact(p.x, p.y)), + &u_h_weights, + &quadrature, + ); + } + let l2_error = l2_error_squared.sqrt(); + l2_error +} + +#[allow(non_snake_case)] +fn main() -> Result<(), Box> { + use std::f64::consts::PI; + let sin = |x| f64::sin(x); + // let cos = |x| f64::cos(x); + let u_exact = |x, y| sin(PI * x) * sin(PI * y); + // let u_grad_exact = |x, y| Vector2::new(PI * cos(PI * x) * sin(PI * y), + // PI * sin(PI * x) * cos(PI * y)); + let f = |x, y| 2.0 * PI * PI * u_exact(x, y); + + let resolutions = vec![1, 2, 3, 4, 5, 6, 7, 14, 28]; + + let error_quadrature = quad_quadrature_strength_11(); + for res in resolutions { + let problem = TestProblem { + solution: u_exact, + rhs: f, + mesh: create_rectangular_uniform_quad_mesh_2d(1.0, 2, 2, res, &Vector2::new(-1.0, 1.0)), + }; + let u_h = solve_2d_test_problem(&problem)?; + let l2_error = estimate_l2_error(&problem.mesh, &u_h, u_exact, &error_quadrature); + println!("L2 error: {}", l2_error); + } + + Ok(()) +} diff --git a/fenris/examples/poisson_common/mod.rs b/fenris/examples/poisson_common/mod.rs new file mode 100644 index 0000000..af208a5 --- /dev/null +++ b/fenris/examples/poisson_common/mod.rs @@ -0,0 +1,33 @@ +use fenris::assembly::{GeneralizedEllipticContraction, GeneralizedEllipticOperator}; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, DimName, Matrix1, RealField, Scalar, VectorN, U1}; + +pub struct PoissonEllipticOperator; + +impl GeneralizedEllipticOperator for PoissonEllipticOperator +where + T: Scalar, + GeometryDim: DimName, + DefaultAllocator: Allocator, +{ + fn compute_elliptic_term(&self, gradient: &VectorN) -> VectorN { + gradient.clone_owned() + } +} + +impl GeneralizedEllipticContraction for PoissonEllipticOperator +where + T: RealField, + GeometryDim: DimName, + DefaultAllocator: + Allocator + Allocator + Allocator, +{ + fn contract( + &self, + _gradient: &VectorN, + a: &VectorN, + b: &VectorN, + ) -> Matrix1 { + Matrix1::new(a.dot(&b)) + } +} diff --git a/fenris/examples/poisson_mms.rs b/fenris/examples/poisson_mms.rs new file mode 100644 index 0000000..d2accf4 --- /dev/null +++ b/fenris/examples/poisson_mms.rs @@ -0,0 +1,132 @@ +mod poisson_common; +use poisson_common::*; + +use fenris::assembly::{ + apply_homogeneous_dirichlet_bc_csr, apply_homogeneous_dirichlet_bc_rhs, assemble_generalized_stiffness, + assemble_source_term_into, +}; +use fenris::element::ElementConnectivity; +use fenris::error::estimate_element_L2_error_squared; +use fenris::geometry::procedural::create_rectangular_uniform_hex_mesh; +use fenris::geometry::vtk::write_vtk; +use fenris::mesh::Hex20Mesh; +use fenris::quadrature::hex_quadrature_strength_5; +use fenris::vtkio::model::Attribute; +use fenris::vtkio::IOBuffer; +use mkl_corrode::dss; +use mkl_corrode::dss::Definiteness; +use mkl_corrode::dss::MatrixStructure::Symmetric; +use nalgebra::storage::Storage; +use nalgebra::{DVector, DVectorSlice, DVectorSliceMut, Vector1, Vector3, U1}; +use std::error::Error; +use std::ops::Add; +use vtkio::model::DataSet; + +#[allow(non_snake_case)] +fn main() -> Result<(), Box> { + use std::f64::consts::PI; + let sin = |x| f64::sin(x); + + let u_exact_xyz = |x, y, z| sin(PI * x) * sin(PI * y) * sin(PI * z); + let u_exact = |x: &Vector3| -> Vector1 { Vector1::new(u_exact_xyz(x.x, x.y, x.z)) }; + let f_xyz = { |x, y, z| 3.0 * PI * PI * u_exact_xyz(x, y, z) }; + let f = |x: &Vector3| Vector1::new(f_xyz(x.x, x.y, x.z)); + let resolutions = vec![1, 2, 4, 8, 16]; + + for res in resolutions { + let mesh = create_rectangular_uniform_hex_mesh(1.0, 1, 1, 1, res); + // let mesh = Tet4Mesh::try_from(&PolyMesh3d::from(&mesh).triangulate()?)?; + // let mesh = Tet20Mesh::from(&mesh); + // let mesh = Tet10Mesh::from(&mesh); + let mesh = Hex20Mesh::from(&mesh); + let quadrature = hex_quadrature_strength_5(); + // let quadrature = tet_quadrature_strength_5(); + let ndof = mesh.vertices().len(); + + // Assemble system matrix and right hand side + let qtable = |_| &quadrature; + let u = DVector::zeros(ndof); + let operator = PoissonEllipticOperator; + let mut A = assemble_generalized_stiffness(mesh.vertices(), mesh.connectivity(), &operator, &u, &qtable) + .to_csr(Add::add); + let boundary_vertices: Vec<_> = mesh + .vertices() + .iter() + .enumerate() + .filter(|(_, v)| (v.coords - Vector3::new(0.5, 0.5, 0.5)).amax() >= 0.499) + .map(|(i, _)| i) + .collect(); + + let mut rhs = DVector::zeros(ndof); + assemble_source_term_into(DVectorSliceMut::from(&mut rhs), &mesh, &f, &qtable); + + apply_homogeneous_dirichlet_bc_csr::<_, U1>(&mut A, &boundary_vertices); + apply_homogeneous_dirichlet_bc_rhs(&mut rhs, &boundary_vertices, 1); + + let A_dss = + dss::SparseMatrix::try_convert_from_csr(A.row_offsets(), A.column_indices(), A.values(), Symmetric)?; + let options = dss::SolverOptions::default().parallel_reorder(true); + let mut solver = dss::Solver::try_factor_with_opts(&A_dss, Definiteness::PositiveDefinite, &options)?; + let u_solution = solver.solve(rhs.data.as_slice()).unwrap(); + + { + let mut dataset = DataSet::from(&mesh); + if let DataSet::UnstructuredGrid { ref mut data, .. } = dataset { + let u_buffer = IOBuffer::from_slice(u_solution.as_slice()); + let attribute = Attribute::Scalars { + num_comp: 1, + lookup_table: None, + data: u_buffer, + }; + data.point.push((format!("u"), attribute)); + } else { + panic!("Unexpected data"); + } + + write_vtk(dataset, format!("poisson_sol_{}.vtk", res), "Poisson solution")?; + } + + // Write nodal interpolated data + { + let mut dataset = DataSet::from(&mesh); + let u_nodal_interpolation: Vec<_> = mesh + .vertices() + .iter() + .map(|v| u_exact(&v.coords).x) + .collect(); + if let DataSet::UnstructuredGrid { ref mut data, .. } = dataset { + let u_buffer = IOBuffer::from_slice(u_nodal_interpolation.as_slice()); + let attribute = Attribute::Scalars { + num_comp: 1, + lookup_table: None, + data: u_buffer, + }; + data.point.push((format!("u"), attribute)); + } else { + panic!("Unexpected data"); + } + + write_vtk( + dataset, + format!("poisson_nodal_interpolation_{}.vtk", res), + "Poisson solution", + )?; + } + + let l2_error_squared = mesh + .connectivity() + .iter() + .map(|conn| { + let u_weights = + conn.element_variables(DVectorSlice::from_slice(u_solution.as_slice(), u_solution.len())); + let element = conn.element(mesh.vertices()).unwrap(); + estimate_element_L2_error_squared(&element, |x, _| u_exact(&x.coords), &u_weights, &quadrature) + }) + .sum::(); + let l2_error = l2_error_squared.sqrt(); + + println!("L2 error: {:3.3e}", l2_error); + } + + Ok(()) +} diff --git a/fenris/examples/polymesh_intersection.rs b/fenris/examples/polymesh_intersection.rs new file mode 100644 index 0000000..2079cd8 --- /dev/null +++ b/fenris/examples/polymesh_intersection.rs @@ -0,0 +1,106 @@ +use fenris::geometry::polymesh::PolyMesh3d; +use fenris::geometry::procedural::create_rectangular_uniform_hex_mesh; +use fenris::geometry::vtk::write_vtk; +use fenris::geometry::{ConvexPolyhedron, HalfSpace, Tetrahedron}; +use fenris::mesh::Tet4Mesh; +use nalgebra::{Point3, Unit, Vector3}; +use nested_vec::NestedVec; +use std::convert::TryFrom; +use std::error::Error; + +fn tetrahedron_vertices() -> [Point3; 4] { + [ + Point3::new(0.1, 0.1, 0.1), + Point3::new(0.9, 0.1, 0.1), + Point3::new(0.1, 0.9, 0.1), + Point3::new(0.1, 0.1, 0.9), + ] +} + +fn tetrahedron() -> PolyMesh3d { + let vertices = tetrahedron_vertices().to_vec(); + let faces = vec![vec![0, 2, 1], vec![0, 1, 3], vec![1, 2, 3], vec![0, 3, 2]]; + let cells = vec![vec![0, 1, 2, 3]]; + PolyMesh3d::from_poly_data(vertices, NestedVec::from(&faces), NestedVec::from(&cells)) +} + +fn intersect_meshes_with_single_half_space() -> Result<(), Box> { + let cube = create_rectangular_uniform_hex_mesh(1.0, 2, 1, 1, 1); + let cube = PolyMesh3d::from(&cube); + + // Intersect meshes with half space + let meshes = vec![("cube", cube), ("tetrahedron", tetrahedron())]; + + for (name, mesh) in meshes { + let half_space = HalfSpace::from_point_and_normal( + Point3::new(0.0, 0.0, 0.3), + Unit::new_normalize(Vector3::new(0.0, 0.0, 1.0)), + ); + + let intersection = mesh.intersect_half_space(&half_space); + + let base_path = "data/polymesh_intersection/halfspace"; + let original_mesh_file_name = format!("{}/{}.vtk", base_path, name); + let intersection_file_name = format!("{}/{}_intersection.vtk", base_path, name); + + write_vtk(&mesh, original_mesh_file_name, "polymesh intersection")?; + write_vtk(&intersection, intersection_file_name, "polymesh intersection")?; + + println!("Original mesh: {}", mesh); + println!("Intersection: {}", intersection); + } + Ok(()) +} + +fn intersect_polyhedron_with_mesh<'a>( + mesh_name: &str, + polyhedron_name: &str, + polyhedron: &impl ConvexPolyhedron<'a, f64>, + mesh: &PolyMesh3d, +) -> Result<(), Box> { + let intersection = mesh.intersect_convex_polyhedron(polyhedron); + + let base_path = "data/polymesh_intersection/polyhedra"; + let original_mesh_file_name = format!("{}/{}.vtk", base_path, mesh_name); + let intersection_file_name = format!("{}/{}_{}_intersection.vtk", base_path, mesh_name, polyhedron_name); + let tet_mesh_file_name = format!( + "{}/{}_{}_intersection_tet_mesh.vtk", + base_path, mesh_name, polyhedron_name + ); + + write_vtk(mesh, original_mesh_file_name, "polymesh intersection")?; + write_vtk(&intersection, intersection_file_name, "polymesh intersection")?; + + println!("Original mesh: {}", mesh); + println!("Intersection: {}", intersection); + + let tet_mesh = Tet4Mesh::try_from(&intersection.triangulate()?)?; + write_vtk(&tet_mesh, tet_mesh_file_name, "tet mesh")?; + + Ok(()) +} + +fn intersect_meshes_with_polyhedra() -> Result<(), Box> { + let cube = create_rectangular_uniform_hex_mesh(1.0, 1, 1, 1, 2); + let cube = PolyMesh3d::from(&cube); + + // Intersect meshes with half space + let meshes = vec![("cube", cube), ("tetrahedron", tetrahedron())]; + + { + let tet = Tetrahedron::from_vertices(tetrahedron_vertices()); + let name = "tet"; + for (mesh_name, mesh) in &meshes { + intersect_polyhedron_with_mesh(mesh_name, name, &tet, mesh)?; + } + } + + Ok(()) +} + +fn main() -> Result<(), Box> { + intersect_meshes_with_single_half_space()?; + intersect_meshes_with_polyhedra()?; + + Ok(()) +} diff --git a/fenris/src/allocators.rs b/fenris/src/allocators.rs new file mode 100644 index 0000000..9492342 --- /dev/null +++ b/fenris/src/allocators.rs @@ -0,0 +1,200 @@ +//! Helper traits for collecting element allocator trait bounds. + +use crate::element::{ConnectivityGeometryDim, ConnectivityNodalDim, ConnectivityReferenceDim, ElementConnectivity}; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, DimName, DimNameMul, DimNameProd, Scalar, U1}; + +/// Helper trait to make specifying bounds on generic functions working with the +/// `ReferenceFiniteElement` trait easier. +pub trait ReferenceFiniteElementAllocator: +Allocator ++ Allocator ++ Allocator ++ Allocator ++ Allocator ++ Allocator ++ Allocator +// For representing the indices of the nodes ++ Allocator ++ Allocator<(usize, usize), NodalDim> ++ Allocator<(usize, usize), ReferenceDim> + where + T: Scalar, + ReferenceDim: DimName, + NodalDim: DimName, +{ + +} + +/// Helper trait to make specifying bounds on generic functions working with the +/// `FiniteElement` trait easier. +pub trait FiniteElementAllocator: +ReferenceFiniteElementAllocator ++ Allocator ++ Allocator ++ Allocator ++ Allocator ++ Allocator ++ Allocator ++ Allocator ++ Allocator ++ Allocator ++ Allocator ++ Allocator ++ Allocator ++ Allocator ++ Allocator ++ Allocator +// For representing the indices of the nodes ++ Allocator ++ Allocator<(usize, usize), GeometryDim> ++ Allocator<(usize, usize), NodalDim> ++ Allocator<(usize, usize), ReferenceDim> + where + T: Scalar, + GeometryDim: DimName, + ReferenceDim: DimName, + NodalDim: DimName, +{ + +} + +/// Helper trait to make specifying bounds on generic functions working with the +/// `FiniteElement` trait easier, for elements whose geometry dimension and reference element +/// dimension coincide. +pub trait VolumeFiniteElementAllocator: + FiniteElementAllocator +where + T: Scalar, + GeometryDim: DimName, + NodalDim: DimName, +{ +} + +/// Helper trait to simplify specifying bounds on generic functions that need to +/// construct element (mass/stiffness) matrices when working with the `FiniteElement` trait. +pub trait FiniteElementMatrixAllocator: + VolumeFiniteElementAllocator + + Allocator, DimNameProd> + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator<(usize, usize), SolutionDim> +where + T: Scalar, + GeometryDim: DimName, + SolutionDim: DimNameMul, + NodalDim: DimName, +{ +} + +impl ReferenceFiniteElementAllocator for DefaultAllocator +where + T: Scalar, + ReferenceDim: DimName, + NodalDim: DimName, + DefaultAllocator: Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator<(usize, usize), NodalDim> + + Allocator<(usize, usize), ReferenceDim>, +{ +} + +impl FiniteElementAllocator + for DefaultAllocator +where + T: Scalar, + GeometryDim: DimName, + NodalDim: DimName, + ReferenceDim: DimName, + DefaultAllocator: ReferenceFiniteElementAllocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator<(usize, usize), GeometryDim> + + Allocator<(usize, usize), NodalDim> + + Allocator<(usize, usize), ReferenceDim>, +{ +} + +impl FiniteElementMatrixAllocator + for DefaultAllocator +where + T: Scalar, + SolutionDim: DimNameMul, + GeometryDim: DimName, + NodalDim: DimName, + DefaultAllocator: VolumeFiniteElementAllocator + + Allocator, DimNameProd> + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator<(usize, usize), SolutionDim>, +{ +} + +impl VolumeFiniteElementAllocator for DefaultAllocator +where + T: Scalar, + GeometryDim: DimName, + NodalDim: DimName, + DefaultAllocator: FiniteElementAllocator, +{ +} + +pub trait ElementConnectivityAllocator: + FiniteElementAllocator< + T, + ConnectivityGeometryDim, + ConnectivityReferenceDim, + ConnectivityNodalDim, +> +where + T: Scalar, + Connectivity: ElementConnectivity, + DefaultAllocator: FiniteElementAllocator< + T, + ConnectivityGeometryDim, + ConnectivityReferenceDim, + ConnectivityNodalDim, + >, +{ +} + +impl ElementConnectivityAllocator for DefaultAllocator +where + T: Scalar, + C: ElementConnectivity, + DefaultAllocator: FiniteElementAllocator< + T, + ConnectivityGeometryDim, + ConnectivityReferenceDim, + ConnectivityNodalDim, + >, +{ +} diff --git a/fenris/src/assembly.rs b/fenris/src/assembly.rs new file mode 100644 index 0000000..f214c40 --- /dev/null +++ b/fenris/src/assembly.rs @@ -0,0 +1,1468 @@ +use crate::element::{ElementConnectivity, VolumetricFiniteElement}; +use crate::quadrature::Quadrature; +use nalgebra::dimension::{DimNameMul, DimNameProd}; +use nalgebra::{ + ComplexField, DMatrix, DVector, DVectorSlice, DVectorSliceMut, DefaultAllocator, Dim, DimMin, DimName, Matrix, + MatrixMN, MatrixN, Point, RealField, Scalar, SquareMatrix, VectorN, U1, +}; +use nalgebra::{DMatrixSliceMut, Dynamic, MatrixSliceMN}; +use std::ops::AddAssign; + +use nalgebra::SymmetricEigen; + +use crate::allocators::{ElementConnectivityAllocator, FiniteElementMatrixAllocator, VolumeFiniteElementAllocator}; +use crate::sparse::{CsrRowMut, SparsityPattern}; +use crate::{CooMatrix, CsrMatrix}; +use alga::general::{ClosedAdd, ClosedMul}; +use nalgebra::allocator::Allocator; +use nalgebra::storage::Storage; +use num::{One, Zero}; +use rayon::prelude::*; + +use crate::connectivity::Connectivity; +use crate::mesh::Mesh; +use crate::space::FiniteElementSpace; +use crate::util::{coerce_col_major_slice, coerce_col_major_slice_mut}; +use nested_vec::NestedVec; +use paradis::adapter::BlockAdapter; +use paradis::coloring::sequential_greedy_coloring; +use paradis::DisjointSubsets; +use std::cell::RefCell; +use std::error::Error; +use std::marker::PhantomData; +use thread_local::ThreadLocal; + +pub trait ElementConnectivityAssembler { + fn solution_dim(&self) -> usize; + + fn num_elements(&self) -> usize; + + fn num_nodes(&self) -> usize; + + fn element_node_count(&self, element_index: usize) -> usize; + + fn populate_element_nodes(&self, output: &mut [usize], element_index: usize); +} + +impl ElementConnectivityAssembler for Mesh +where + T: Scalar, + D: DimName, + C: Connectivity, + DefaultAllocator: Allocator, +{ + fn solution_dim(&self) -> usize { + 1 + } + + fn num_elements(&self) -> usize { + self.connectivity().len() + } + + fn num_nodes(&self) -> usize { + self.vertices().len() + } + + fn element_node_count(&self, element_index: usize) -> usize { + self.connectivity()[element_index].vertex_indices().len() + } + + fn populate_element_nodes(&self, output: &mut [usize], element_index: usize) { + output.copy_from_slice(self.connectivity()[element_index].vertex_indices()); + } +} + +pub trait ElementAssembler: ElementConnectivityAssembler { + fn assemble_element_matrix_into( + &self, + output: DMatrixSliceMut, + element_index: usize, + ) -> Result<(), Box>; +} + +/// An assembler for CSR matrices. +#[derive(Debug, Clone)] +pub struct CsrAssembler { + // All members are buffers that help prevent unnecessary allocations + // when assembling multiple matrices with the same assembler + connectivity_permutation: Vec, + element_global_nodes: Vec, + element_matrix: DMatrix, +} + +impl Default for CsrAssembler { + fn default() -> Self { + Self { + connectivity_permutation: Vec::new(), + element_global_nodes: Vec::new(), + element_matrix: DMatrix::zeros(0, 0), + } + } +} + +impl CsrAssembler { + pub fn assemble_into_csr( + &mut self, + csr: &mut CsrMatrix, + element_assembler: &dyn ElementAssembler, + ) -> Result<(), Box> { + // Reuse previously allocated buffers + let connectivity_permutation = &mut self.connectivity_permutation; + let element_global_nodes = &mut self.element_global_nodes; + let element_matrix = &mut self.element_matrix; + + let sdim = element_assembler.solution_dim(); + + for i in 0..element_assembler.num_elements() { + let element_node_count = element_assembler.element_node_count(i); + let element_matrix_dim = sdim * element_node_count; + + element_global_nodes.resize(element_node_count, 0); + element_matrix.resize_mut(element_matrix_dim, element_matrix_dim, T::zero()); + element_matrix.fill(T::zero()); + + let matrix_slice = DMatrixSliceMut::from(&mut *element_matrix); + element_assembler.assemble_element_matrix_into(matrix_slice, i)?; + element_assembler.populate_element_nodes(element_global_nodes, i); + + connectivity_permutation.clear(); + connectivity_permutation.extend(0..element_node_count); + connectivity_permutation.sort_unstable_by_key(|i| element_global_nodes[*i]); + + for (local_node_idx, global_node_idx) in element_global_nodes.iter().enumerate() { + for i in 0..sdim { + let local_row_index = sdim * local_node_idx + i; + let global_row_index = sdim * *global_node_idx + i; + let mut csr_row = csr.row_mut(global_row_index); + + let a_row = element_matrix.row(local_row_index); + add_element_row_to_csr_row( + &mut csr_row, + &element_global_nodes, + &connectivity_permutation, + sdim, + &a_row, + ); + } + } + } + + Ok(()) + } +} + +/// A parallel assembler for CSR matrices relying on a graph coloring of elements. +/// +/// TODO: Consider using type erasure to store buffers without needing the generic type parameter +#[derive(Debug)] +pub struct CsrParAssembler { + // All members are buffers that help prevent unnecessary allocations + // when assembling multiple matrices with the same assembler + connectivity_permutation: ThreadLocal>>, + element_global_nodes: ThreadLocal>>, + element_matrix: ThreadLocal>>, +} + +impl Default for CsrParAssembler { + fn default() -> Self { + Self { + connectivity_permutation: ThreadLocal::new(), + element_global_nodes: ThreadLocal::new(), + element_matrix: ThreadLocal::new(), + } + } +} + +impl CsrParAssembler { + pub fn assemble_pattern(&self, element_assembler: &(dyn Sync + ElementConnectivityAssembler)) -> SparsityPattern { + let sdim = element_assembler.solution_dim(); + + // Count number of (including duplicate) triplets + let num_total_triplets = (0..element_assembler.num_elements()) + .into_par_iter() + .with_min_len(50) + .map(|element_idx| { + let num_entries = sdim * element_assembler.element_node_count(element_idx); + num_entries * num_entries + }) + .sum(); + + // TODO: Can we do this next stage in parallel somehow? + // (it is however entirely memory bound, but a single thread + // probably cannot exhaust that on its own) + let mut coordinates = Vec::with_capacity(num_total_triplets); + let mut index_workspace = Vec::new(); + for element_idx in 0..element_assembler.num_elements() { + let node_count = element_assembler.element_node_count(element_idx); + index_workspace.resize(node_count, 0); + element_assembler.populate_element_nodes(&mut index_workspace, element_idx); + + for node_i in &index_workspace { + for node_j in &index_workspace { + for i in 0..sdim { + for j in 0..sdim { + coordinates.push((sdim * node_i + i, sdim * node_j + j)); + } + } + } + } + } + + coordinates.par_sort_unstable(); + + // TODO: Can we parallelize the final part? + // TODO: move this into something like SparsityPattern::from_coordinates ? + // But then we'd probably also have to deal with the case in which + // the coordinates are perhaps not sorted (either error out or + // deal with it on the fly) + let num_rows = sdim * element_assembler.num_nodes(); + let mut row_offsets = Vec::with_capacity(num_rows); + let mut column_indices = Vec::new(); + row_offsets.push(0); + + let mut coord_iter = coordinates.into_iter(); + let mut current_row = 0; + let mut prev_col = None; + + while let Some((i, j)) = coord_iter.next() { + assert!(i < num_rows, "Coordinates must be in bounds"); + + while i > current_row { + row_offsets.push(column_indices.len()); + current_row += 1; + prev_col = None; + } + + // Only add column if it is not a duplicate + if Some(j) != prev_col { + column_indices.push(j); + prev_col = Some(j); + } + } + + // Fill out offsets for remaining empty rows + for _ in current_row..num_rows { + row_offsets.push(column_indices.len()); + } + + SparsityPattern::from_offsets_and_indices(num_rows, num_rows, row_offsets, column_indices) + } +} + +impl CsrParAssembler { + pub fn assemble_into_csr( + &mut self, + csr: &mut CsrMatrix, + colors: &[DisjointSubsets], + element_assembler: &(dyn Sync + ElementAssembler), + ) -> Result<(), Box> { + let sdim = element_assembler.solution_dim(); + + for color in colors { + let mut block_adapter = BlockAdapter::with_block_size(csr, sdim); + color + .subsets_par_iter(&mut block_adapter) + .map(|mut subset| { + let mut connectivity_permutation = self.connectivity_permutation.get_or_default().borrow_mut(); + let mut element_global_nodes = self.element_global_nodes.get_or_default().borrow_mut(); + let mut element_matrix = self + .element_matrix + .get_or(|| RefCell::new(DMatrix::zeros(0, 0))) + .borrow_mut(); + + let element_index = subset.label(); + let element_node_count = element_assembler.element_node_count(element_index); + let element_matrix_dim = sdim * element_node_count; + + element_global_nodes.resize(element_node_count, 0); + element_matrix.resize_mut(element_matrix_dim, element_matrix_dim, T::zero()); + element_matrix.fill(T::zero()); + + let matrix_slice = DMatrixSliceMut::from(&mut *element_matrix); + element_assembler.assemble_element_matrix_into(matrix_slice, element_index)?; + element_assembler.populate_element_nodes(&mut element_global_nodes, element_index); + debug_assert_eq!(subset.global_indices(), element_global_nodes.as_slice()); + + connectivity_permutation.clear(); + connectivity_permutation.extend(0..element_node_count); + connectivity_permutation.sort_unstable_by_key(|i| element_global_nodes[*i]); + + for local_node_idx in 0..element_node_count { + let mut csr_block_row = subset.get_mut(local_node_idx); + for i in 0..sdim { + let local_row_index = sdim * local_node_idx + i; + let mut csr_row = csr_block_row.get_mut(i).unwrap(); + + let a_row = element_matrix.row(local_row_index); + add_element_row_to_csr_row( + &mut csr_row, + &element_global_nodes, + &connectivity_permutation, + sdim, + &a_row, + ); + } + } + + Ok(()) + }) + .collect::>>()?; + } + + Ok(()) + } +} + +struct GeneralizedStiffnessElementAssembler<'a, T, SolutionDim, C, Contraction, Q, Transformation> +where + T: Scalar, + C: ElementConnectivity>::GeometryDim>, + Transformation: ?Sized, + DefaultAllocator: ElementConnectivityAllocator, +{ + vertices: &'a [Point], + connectivity: &'a [C], + contraction: &'a Contraction, + u: &'a DVector, + quadrature_table: &'a Q, + transformation: &'a Transformation, + solution_dim_marker: PhantomData, +} + +impl<'a, T, SolutionDim, C, Contraction, Q, Transformation> ElementConnectivityAssembler + for GeneralizedStiffnessElementAssembler<'a, T, SolutionDim, C, Contraction, Q, Transformation> +where + T: Scalar, + C: ElementConnectivity>::GeometryDim>, + SolutionDim: DimName, + Transformation: ?Sized, + DefaultAllocator: ElementConnectivityAllocator, +{ + fn num_nodes(&self) -> usize { + self.vertices.len() + } + + fn solution_dim(&self) -> usize { + SolutionDim::dim() + } + + fn num_elements(&self) -> usize { + self.connectivity.len() + } + + fn element_node_count(&self, element_index: usize) -> usize { + self.connectivity[element_index].vertex_indices().len() + } + + fn populate_element_nodes(&self, output: &mut [usize], element_index: usize) { + output.copy_from_slice(self.connectivity[element_index].vertex_indices()); + } +} + +impl<'a, T, SolutionDim, C, Contraction, Q, Transformation> ElementAssembler + for GeneralizedStiffnessElementAssembler<'a, T, SolutionDim, C, Contraction, Q, Transformation> +where + T: RealField, + C: ElementConnectivity>::GeometryDim>, + C::GeometryDim: DimMin, + SolutionDim: DimNameMul, + Contraction: GeneralizedEllipticContraction, + Q: QuadratureTable, + Transformation: ?Sized + ElementMatrixTransformation, + DefaultAllocator: FiniteElementMatrixAllocator, +{ + fn assemble_element_matrix_into( + &self, + mut output: DMatrixSliceMut, + element_index: usize, + ) -> Result<(), Box> { + let connectivity = &self.connectivity[element_index]; + let element = connectivity.element(self.vertices).expect( + "All vertices of element are assumed to be in bounds.\ + TODO: Ensure this upon construction of basis?", + ); + let u_element = connectivity.element_variables(self.u); + // TODO: Assemble directly into output + let a_element = assemble_generalized_element_stiffness( + &element, + self.contraction, + &u_element, + &self.quadrature_table.quadrature_for_element(element_index), + ); + + assert_eq!(output.nrows(), a_element.nrows()); + assert_eq!(output.ncols(), a_element.ncols()); + output.copy_from(&a_element); + + self.transformation.transform_element_matrix(&mut output); + + Ok(()) + } +} + +pub trait GeneralizedEllipticOperator +where + T: Scalar, + SolutionDim: DimName, + GeometryDim: DimName, + DefaultAllocator: Allocator, +{ + fn compute_elliptic_term( + &self, + gradient: &MatrixMN, + ) -> MatrixMN; +} + +pub trait GeneralizedEllipticContraction +where + T: Scalar + Zero + One + ClosedAdd + ClosedMul, + SolutionDim: DimName, + GeometryDim: DimName, + DefaultAllocator: Allocator + + Allocator + + Allocator + + Allocator + + Allocator, +{ + fn contract( + &self, + gradient: &MatrixMN, + a: &VectorN, + b: &VectorN, + ) -> MatrixMN; + + /// Compute multiple contractions and store the result in the provided matrix. + /// + /// The matrix `a` is a `GeometryDim x NodalDim` sized matrix, in which each column + /// corresponds to a vector of dimension `GeometryDim`. The output matrix is a square matrix + /// with row and col dimensions `SolutionDim * NodalDim`, consisting of `NodalDim x NodalDim` + /// block matrices, each with dimension `SolutionDim x SolutionDim`. + /// + /// Let c(gradient, a, b) denote the contraction of vectors a and b. + /// Then the result of c(gradient, a_I, a_J) for each I, J in the range `(0 .. NodalDim)` + /// must be *added* to `output_IJ`, where `output_IJ` is the `SolutionDim x SolutionDim` + /// block matrix corresponding to nodes `I` and `J`. + /// + /// TODO: Consider using a unit-stride matrix slice for performance reasons. + fn contract_multiple_into( + &self, + output: &mut DMatrixSliceMut, + gradient: &MatrixMN, + a: &MatrixSliceMN, + ) { + let num_nodes = a.ncols(); + let output_dim = num_nodes * SolutionDim::dim(); + assert_eq!(output_dim, output.nrows()); + assert_eq!(output_dim, output.ncols()); + + let sdim = SolutionDim::dim(); + for i in 0..num_nodes { + for j in i..num_nodes { + let a_i = a.fixed_slice::(0, i).clone_owned(); + let a_j = a.fixed_slice::(0, j).clone_owned(); + let contraction = self.contract(gradient, &a_i, &a_j); + output + .fixed_slice_mut::(i * sdim, j * sdim) + .add_assign(&contraction); + + // TODO: We currently assume symmetry. Should maybe have a method that + // says whether it is symmetric or not? + if i != j { + output + .fixed_slice_mut::(j * sdim, i * sdim) + .add_assign(&contraction.transpose()); + } + } + } + } +} + +/// Lookup table mapping elements to quadrature rules. +pub trait QuadratureTable +where + T: Scalar, + GeometryDim: DimName, + DefaultAllocator: Allocator, +{ + type QuadratureRule: Quadrature; + + fn quadrature_for_element(&self, element_index: usize) -> Self::QuadratureRule; +} + +impl<'a, T, GeometryDim, F, Q> QuadratureTable for F +where + F: 'a + Fn(usize) -> Q, + Q: 'a + Quadrature, + T: Scalar, + GeometryDim: DimName, + DefaultAllocator: Allocator, +{ + type QuadratureRule = Q; + + fn quadrature_for_element(&self, element_index: usize) -> Self::QuadratureRule { + self(element_index) + } +} + +/// Convenience wrapper to turn a single quadrature into a quadrature table. +/// +/// More precisely, this implies that the same quadrature rule will be used for every +/// element. +/// +/// Note that the given quadrature will be cloned, so it's often more useful to wrap +/// a reference to a quadrature than letting the quadrature be cloned for each element. +#[derive(Copy, Clone, Debug)] +pub struct UniformQuadratureTable(pub Q); + +impl QuadratureTable for UniformQuadratureTable +where + T: Scalar, + GeometryDim: DimName, + DefaultAllocator: Allocator, + Q: Clone + Quadrature, +{ + type QuadratureRule = Q; + + fn quadrature_for_element(&self, _element_index: usize) -> Self::QuadratureRule { + self.0.clone() + } +} + +/// A transformation for element matrices. +/// +/// This is most often used to adapt the spectrum of an element matrix so that it +/// becomes semi-definite. +pub trait ElementMatrixTransformation { + fn transform_element_matrix(&self, element_matrix: &mut DMatrixSliceMut); +} + +/// Leaves the given element matrix unaltered. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct NoTransformation; + +impl ElementMatrixTransformation for NoTransformation { + fn transform_element_matrix(&self, _element_matrix: &mut DMatrixSliceMut) { + // Do nothing + } +} + +/// Projects the given matrix onto semidefiniteness by using `nalgebra`'s symmetric +/// eigendecomposition. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct DefaultSemidefiniteProjection; + +impl ElementMatrixTransformation for DefaultSemidefiniteProjection { + fn transform_element_matrix(&self, element_matrix: &mut DMatrixSliceMut) { + let mut eigendecomp = SymmetricEigen::new(element_matrix.clone_owned()); + for eigenval in &mut eigendecomp.eigenvalues { + *eigenval = T::max(T::zero(), *eigenval); + } + // TODO: Don't recompose if we didn't change anything + let recomposed = eigendecomp.recompose(); + element_matrix.copy_from(&recomposed); + } +} + +pub type ElementMatrix = MatrixN>; + +/// Computes the integral of a scalar function f(X, u, grad u) over an element. +#[allow(non_snake_case)] +pub fn compute_element_integral( + element: &Element, + u_element: &MatrixMN, + quadrature: &impl Quadrature, + function: F, +) -> T +where + T: RealField, + Element: VolumetricFiniteElement, + Element::GeometryDim: DimName + DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: VolumeFiniteElementAllocator + + Allocator + + Allocator + + Allocator + + Allocator, + F: Fn( + &VectorN, + &VectorN, + &MatrixMN, + ) -> T, +{ + let mut f_e = T::zero(); + + let weights = quadrature.weights(); + let points = quadrature.points(); + + for (&w, xi) in weights.iter().zip(points) { + let phi = element.evaluate_basis(xi); + let G = element.gradients(xi); + + // Jacobian + let J = element.reference_jacobian(xi); + + let J_det = J.determinant(); + let J_inv = J.try_inverse().expect("Jacobian must be invertible"); + + let X = element.map_reference_coords(xi); + let u = u_element * phi.transpose(); + let u_grad = (u_element * (&G.transpose() * &J_inv)).transpose(); + + let f = function(&X, &u, &u_grad); + f_e += f * w * J_det.abs(); + } + + f_e +} + +/// Assemble the generalized element matrix for the given element. +/// +/// TODO: Allow non-uniform density. Possible API: Take callback that gets both index of quadrature +/// point and position of quadrature point...? That way one can i.e. associate density with +/// a particular quadrature point (or return an element-wise constant) or use an analytic function. +#[allow(non_snake_case)] +pub fn assemble_generalized_element_mass( + element: &Element, + density: T, + quadrature: &Q, +) -> ElementMatrix +where + T: RealField, + Element: VolumetricFiniteElement, + Element::GeometryDim: DimMin, + SolutionDim: DimNameMul, + Q: Quadrature, + DefaultAllocator: FiniteElementMatrixAllocator, +{ + let mut m_element_scalar = MatrixN::::zeros(); + + let weights = quadrature.weights(); + let points = quadrature.points(); + + let nodal_dim = Element::NodalDim::dim(); + let sol_dim = SolutionDim::dim(); + + for (&w, xi) in weights.iter().zip(points) { + let J = element.reference_jacobian(xi); + let J_det = J.determinant(); + + let phi = element.evaluate_basis(xi); + + for i in 0..nodal_dim { + for j in 0..nodal_dim { + // Product of shape functions + m_element_scalar[(i, j)] += density * J_det.abs() * w * phi[i] * phi[j]; + } + } + } + + // Assemble the vector-valued element mass matrix by duplicating the elements of the + // scalar-valued mass matrix + let mut m_element = ElementMatrix::::zeros(); + + let skip_shape = (sol_dim.saturating_sub(1), sol_dim.saturating_sub(1)); + for i in 0..sol_dim { + m_element + .slice_with_steps_mut((i, i), (nodal_dim, nodal_dim), skip_shape) + .copy_from(&m_element_scalar); + } + + m_element +} + +#[allow(non_snake_case)] +pub fn assemble_generalized_element_elliptic_term( + element: &Element, + g: &impl GeneralizedEllipticOperator, + u: &MatrixMN, + quadrature: &impl Quadrature, +) -> MatrixMN +where + T: RealField, + Element: VolumetricFiniteElement, + Element::GeometryDim: DimName + DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: VolumeFiniteElementAllocator + + Allocator + + Allocator + + Allocator, +{ + let mut f_e = MatrixMN::::zeros(); + + let weights = quadrature.weights(); + let points = quadrature.points(); + + for (&w, xi) in weights.iter().zip(points) { + let G = element.gradients(xi); + + // Jacobian + let J = element.reference_jacobian(xi); + let J_det = J.determinant(); + + // TODO: Make error instead of panic? + let J_inv = J.try_inverse().expect("Jacobian must be invertible"); + // TODO: Simplify expression + let u_grad = (u * (&G.transpose() * &J_inv)).transpose(); + let g = g.compute_elliptic_term(&u_grad); + f_e += (g.transpose() * J_inv.transpose() * G) * w * J_det.abs(); + } + + f_e +} + +#[allow(non_snake_case)] +pub fn assemble_element_source_term( + element: &Element, + source_function: &impl Fn(&VectorN) -> VectorN, + quadrature: &impl Quadrature, +) -> MatrixMN +where + T: RealField, + Element: VolumetricFiniteElement, + Element::GeometryDim: DimName + DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: VolumeFiniteElementAllocator + + Allocator + + Allocator + + Allocator + + Allocator, +{ + let mut f_e = MatrixMN::::zeros(); + + let weights = quadrature.weights(); + let points = quadrature.points(); + + for (&w, xi) in weights.iter().zip(points) { + let phi = element.evaluate_basis(xi); + let x = element.map_reference_coords(xi); + let f = source_function(&x); + let J_det = element.reference_jacobian(xi).determinant(); + f_e += f * phi * w * J_det.abs(); + } + + f_e +} + +#[allow(non_snake_case)] +pub fn assemble_generalized_element_stiffness( + element: &Element, + contraction: &impl GeneralizedEllipticContraction, + u: &MatrixMN, + quadrature: &impl Quadrature, +) -> ElementMatrix +where + T: RealField, + Element: VolumetricFiniteElement, + Element::GeometryDim: DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, +{ + let mut a_element = ElementMatrix::::zeros(); + + let weights = quadrature.weights(); + let points = quadrature.points(); + + for (&w, xi) in weights.iter().zip(points) { + // Jacobian + let J = element.reference_jacobian(xi); + let J_det = J.determinant(); + + // TODO: Make error instead of panic? + let J_inv = J.try_inverse().expect("Jacobian must be invertible"); + let J_inv_t = J_inv.transpose(); + + let G_ref = element.gradients(xi); + let mut G = J_inv_t * G_ref; + + let u_grad = &G * u.transpose(); + + let scale = w * J_det.abs(); + // We need to multiply the contraction result by the scale factor. + // We do this implicitly by multiplying the basis gradients by its square root. + // This way we don't have to allocate an additional matrix or complicate + // the trait. + G *= scale.sqrt(); + + let (G_rows, _) = G.data.shape(); + let G_slice = coerce_col_major_slice(&G, G_rows, Dynamic::new(G.ncols())); + + let a_rows = a_element.nrows(); + let a_cols = a_element.ncols(); + let mut a_slice = coerce_col_major_slice_mut(&mut a_element, Dynamic::new(a_rows), Dynamic::new(a_cols)); + + contraction.contract_multiple_into(&mut a_slice, &u_grad, &G_slice); + } + + a_element +} + +pub fn apply_homogeneous_dirichlet_bc_csr(matrix: &mut CsrMatrix, nodes: &[usize]) +where + T: RealField, + SolutionDim: DimName, +{ + let d = SolutionDim::dim(); + + // Determine an appropriately scale element to put on the diagonal + // (Simply setting 1 would ignore the scaling of the entries of the matrix, leading + // to potentially poor condition numbers) + + // Here we just take the first non-zero diagonal entry as a representative scale. + // This is cheap and I think reasonably safe option + let scale = matrix + .diag_iter() + .skip_while(|&x| x == T::zero()) + .map(|x| x.abs()) + .next() + .unwrap_or(T::one()); + + // We need to do the following: + // - zero all rows corresponding to Dirichlet nodes + // - zero all columns corresponding to Dirichlet nodes + // - set diagonal entries corresponding to Dirichlet nodes to a non-zero value + // In order to zero all columns, a naive approach would need to visit all elements in the matrix, + // which might be very expensive. + // Instead, we can exploit symmetry to determine that if we visit column j in row i, + // where i corresponds to a Dirichlet node, we would also need to visit row j in order + // to zero out columns. + + let mut dirichlet_membership = vec![false; d * matrix.nrows()]; + let mut rows_to_visit = vec![false; d * matrix.nrows()]; + + for &node in nodes { + for i in 0..d { + let row_idx = d * node + i; + dirichlet_membership[row_idx] = true; + let mut row = matrix.row_mut(row_idx); + let (cols, values) = row.columns_and_values_mut(); + + for (&col_idx, val) in cols.iter().zip(values) { + if col_idx == row_idx { + *val = scale; + } else { + *val = T::zero(); + // If we need to zero out (r, c), then we also need to zero out (c, r), + // so we need to visit column c in r later + rows_to_visit[col_idx] = true; + } + } + } + } + + let row_visit_iter = rows_to_visit + .iter() + .enumerate() + .filter_map(|(index, &should_visit)| if should_visit { Some(index) } else { None }); + for row_index in row_visit_iter { + let row_is_dirichlet = dirichlet_membership[row_index]; + if !row_is_dirichlet { + let mut row = matrix.row_mut(row_index); + let (cols, values) = row.columns_and_values_mut(); + for (local_idx, &global_idx) in cols.iter().enumerate() { + let col_is_dirichlet = dirichlet_membership[global_idx]; + if col_is_dirichlet { + values[local_idx] = T::zero(); + } + } + } + } +} + +pub fn apply_homogeneous_dirichlet_bc_matrix(matrix: &mut DMatrix, nodes: &[usize]) +where + T: RealField, + SolutionDim: DimName, +{ + let d = SolutionDim::dim(); + + // Determine an appropriately scale element to put on the diagonal + // (Simply setting 1 would ignore the scaling of the entries of the matrix, leading + // to potentially poor condition numbers) + let scale = matrix + .diagonal() + .map(|x| x.abs()) + .fold(T::zero(), |a, b| a + b) + / T::from_usize(matrix.nrows()).unwrap(); + + for node in nodes { + for i in 0..d { + let idx = d * node + i; + matrix.index_mut((.., idx)).fill(T::zero()); + matrix.index_mut((idx, ..)).fill(T::zero()); + *matrix.index_mut((idx, idx)) = scale; + } + } +} + +pub fn apply_homogeneous_dirichlet_bc_rhs<'a, T>( + rhs: impl Into>, + nodes: &[usize], + solution_dim: usize, +) where + T: RealField, +{ + let mut rhs = rhs.into(); + let d = solution_dim; + + for node in nodes { + for i in 0..d { + let idx = d * node + i; + *rhs.index_mut(idx) = T::zero(); + } + } +} + +pub fn assemble_generalized_elliptic_term_into_par<'a, T, SolutionDim, Connectivity>( + mut f: DVectorSliceMut, + vertices: &[Point], + connectivity: &[Connectivity], + g: &(impl Sync + GeneralizedEllipticOperator), + u: impl Into>, + quadrature_table: &(impl Sync + QuadratureTable), + colors: &[DisjointSubsets], +) where + T: RealField, + Connectivity: Sync + ElementConnectivity>::GeometryDim>, + Connectivity::GeometryDim: DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: ElementConnectivityAllocator + + Allocator + + Allocator + + Allocator, + >::Buffer: Sync, +{ + let u = u.into(); + let f_slice = f.as_mut_slice(); + + for color in colors { + let mut block_adapter = BlockAdapter::with_block_size(f_slice, SolutionDim::dim()); + color + .subsets_par_iter(&mut block_adapter) + .for_each(|mut subset| { + let connectivity_idx = subset.label(); + let connectivity = &connectivity[connectivity_idx]; + debug_assert_eq!(subset.global_indices(), connectivity.vertex_indices()); + let element = connectivity + .element(vertices) + .expect("All vertices of element are assumed to be in bounds."); + let u_element = connectivity.element_variables(u); + let f_element = assemble_generalized_element_elliptic_term( + &element, + g, + &u_element, + &quadrature_table.quadrature_for_element(connectivity_idx), + ); + + let sdim = SolutionDim::dim(); + for local_idx in 0..connectivity.vertex_indices().len() { + let mut block = subset.get_mut(local_idx); + let f_col = f_element.fixed_slice::(0, local_idx); + for i in 0..sdim { + *block.index_mut(i) += f_col[i]; + } + } + }); + } +} + +pub fn assemble_source_term_into<'a, T, S, SolutionDim, Connectivity>( + mut f: DVectorSliceMut, + space: &S, + source_function: &impl Fn(&VectorN) -> VectorN, + quadrature_table: &impl QuadratureTable, +) where + T: RealField, + S: FiniteElementSpace, + Connectivity: ElementConnectivity>::GeometryDim>, + Connectivity::GeometryDim: DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: ElementConnectivityAllocator + + Allocator + + Allocator + + Allocator + + Allocator, +{ + let vertices = space.vertices(); + for i in 0..space.num_connectivities() { + let connectivity = space.get_connectivity(i).unwrap(); + let element = connectivity.element(vertices).expect( + "All vertices of element are assumed to be in bounds.\ + TODO: Ensure this upon construction of basis?", + ); + let f_element = + assemble_element_source_term(&element, source_function, &quadrature_table.quadrature_for_element(i)); + + distribute_local_to_global_vector(DVectorSliceMut::from(&mut f), connectivity.vertex_indices(), &f_element); + } +} + +pub fn assemble_generalized_elliptic_term_into<'a, T, SolutionDim, Connectivity>( + mut f: DVectorSliceMut, + vertices: &[Point], + connectivity: &[Connectivity], + g: &impl GeneralizedEllipticOperator, + u: impl Into>, + // TODO: Introduce trait for the "element quadrature map"? + quadrature_table: &impl QuadratureTable, +) where + T: RealField, + Connectivity: ElementConnectivity>::GeometryDim>, + Connectivity::GeometryDim: DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: ElementConnectivityAllocator + + Allocator + + Allocator + + Allocator, +{ + let u = u.into(); + for (i, connectivity) in connectivity.iter().enumerate() { + let element = connectivity.element(vertices).expect( + "All vertices of element are assumed to be in bounds.\ + TODO: Ensure this upon construction of basis?", + ); + let u_element = connectivity.element_variables(u); + let f_element = assemble_generalized_element_elliptic_term( + &element, + g, + &u_element, + &quadrature_table.quadrature_for_element(i), + ); + + distribute_local_to_global_vector(DVectorSliceMut::from(&mut f), connectivity.vertex_indices(), &f_element); + } +} + +/// Add a row of a local element matrix to the provided row of a CSR matrix. +/// +/// `node_connectivity`: The global indices of nodes. +/// `sorted_permutation`: The local indices of nodes in the element, ordered such that the +/// corresponding global indices are sorted. +/// `dim`: The solution dimension. +/// `local_row`: The local row of the element matrix that should be added to the CSR matrix. +fn add_element_row_to_csr_row( + row: &mut CsrRowMut, + node_connectivity: &[usize], + sorted_permutation: &[usize], + dim: usize, + local_row: &Matrix, +) where + T: RealField, + S: Storage, +{ + assert_eq!(node_connectivity.len(), sorted_permutation.len()); + assert_eq!(node_connectivity.len() * dim, local_row.ncols()); + assert!(dim >= 1); + + let (column_indices, values) = row.columns_and_values_mut(); + + let mut csr_col_idx_iter = column_indices.iter().copied().enumerate(); + + for &node_local_idx in sorted_permutation { + let node_global_idx = node_connectivity[node_local_idx]; + + for i in 0..dim { + let local_col_idx = dim * node_local_idx + i; + let global_col_index = dim * node_global_idx + i; + + // TODO: If the CSR matrix has a large number of entries in each row, + // an exponential search may be faster than a linear search as we do here + let (local_csr_col_idx, _) = csr_col_idx_iter + .find(|(_, csr_col_idx)| *csr_col_idx == global_col_index) + .expect("Could not find column index associated with node in CSR row"); + values[local_csr_col_idx] += local_row[local_col_idx]; + } + } +} + +pub fn assemble_generalized_stiffness_into_csr( + csr: &mut CsrMatrix, + vertices: &[Point], + connectivity: &[Connectivity], + contraction: &impl GeneralizedEllipticContraction, + u: &DVector, + quadrature_table: &impl QuadratureTable, +) where + T: RealField, + Connectivity: ElementConnectivity>::GeometryDim>, + Connectivity::GeometryDim: DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, +{ + assemble_transformed_generalized_stiffness_into_csr( + csr, + vertices, + connectivity, + contraction, + u, + quadrature_table, + &NoTransformation, + ) +} + +// TODO: Remove this function in favor of using separate global and element assemblers +pub fn assemble_transformed_generalized_stiffness_into_csr( + csr: &mut CsrMatrix, + vertices: &[Point], + connectivity: &[Connectivity], + contraction: &impl GeneralizedEllipticContraction, + u: &DVector, + quadrature_table: &impl QuadratureTable, + transformation: &(impl ?Sized + ElementMatrixTransformation), +) where + T: RealField, + Connectivity: ElementConnectivity>::GeometryDim>, + Connectivity::GeometryDim: DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, +{ + let element_assembler = GeneralizedStiffnessElementAssembler { + vertices, + connectivity, + contraction, + u, + quadrature_table, + transformation, + solution_dim_marker: PhantomData, + }; + + let mut csr_assembler = CsrAssembler::default(); + csr_assembler + .assemble_into_csr(csr, &element_assembler) + .expect("TODO: Propagate error") +} + +pub fn assemble_generalized_stiffness_into_csr_par( + csr: &mut CsrMatrix, + vertices: &[Point], + connectivity: &[Connectivity], + contraction: &(impl Sync + GeneralizedEllipticContraction), + u: &DVector, + quadrature_table: &(impl Sync + QuadratureTable), + colors: &[DisjointSubsets], +) where + T: RealField, + Connectivity: Sync + ElementConnectivity>::GeometryDim>, + Connectivity::GeometryDim: DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, + >::Buffer: Sync, +{ + assemble_transformed_generalized_stiffness_into_csr_par( + csr, + vertices, + connectivity, + contraction, + u, + quadrature_table, + &NoTransformation, + colors, + ) +} + +pub fn assemble_transformed_generalized_stiffness_into_csr_par( + csr: &mut CsrMatrix, + vertices: &[Point], + connectivity: &[Connectivity], + contraction: &(impl Sync + GeneralizedEllipticContraction), + u: &DVector, + quadrature_table: &(impl Sync + QuadratureTable), + transformation: &(impl ?Sized + Sync + ElementMatrixTransformation), + colors: &[DisjointSubsets], +) where + T: RealField, + Connectivity: Sync + ElementConnectivity>::GeometryDim>, + Connectivity::GeometryDim: DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, + >::Buffer: Sync, +{ + let element_assembler = GeneralizedStiffnessElementAssembler { + vertices, + connectivity, + contraction, + u, + quadrature_table, + transformation, + solution_dim_marker: PhantomData, + }; + + let mut csr_assembler = CsrParAssembler::default(); + csr_assembler + .assemble_into_csr(csr, colors, &element_assembler) + .expect("TODO: Propagate error") +} + +pub fn assemble_transformed_generalized_stiffness_into( + coo: &mut CooMatrix, + vertices: &[Point], + connectivity: &[Connectivity], + contraction: &impl GeneralizedEllipticContraction, + u: &DVector, + quadrature_table: &impl QuadratureTable, + transformation: &(impl ?Sized + ElementMatrixTransformation), +) where + T: RealField, + Connectivity: ElementConnectivity>::GeometryDim>, + Connectivity::GeometryDim: DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, +{ + for (i, connectivity) in connectivity.iter().enumerate() { + let element = connectivity.element(vertices).expect( + "All vertices of element are assumed to be in bounds.\ + TODO: Ensure this upon construction of basis?", + ); + let u_element = connectivity.element_variables(u); + let mut a_element = assemble_generalized_element_stiffness( + &element, + contraction, + &u_element, + &quadrature_table.quadrature_for_element(i), + ); + + let a_rows = a_element.nrows(); + let a_cols = a_element.ncols(); + let mut a_dynamic_slice_mut = + coerce_col_major_slice_mut(&mut a_element, Dynamic::new(a_rows), Dynamic::new(a_cols)); + transformation.transform_element_matrix(&mut a_dynamic_slice_mut); + distribute_local_to_global(coo, connectivity.vertex_indices(), &a_element); + } +} + +pub fn assemble_generalized_stiffness_into( + coo: &mut CooMatrix, + vertices: &[Point], + connectivity: &[Connectivity], + contraction: &impl GeneralizedEllipticContraction, + u: &DVector, + quadrature_table: &impl QuadratureTable, +) where + T: RealField, + Connectivity: ElementConnectivity>::GeometryDim>, + Connectivity::GeometryDim: DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, +{ + assemble_transformed_generalized_stiffness_into( + coo, + vertices, + connectivity, + contraction, + u, + quadrature_table, + &NoTransformation, + ) +} + +pub fn assemble_generalized_stiffness( + vertices: &[Point], + connectivity: &[Connectivity], + contraction: &impl GeneralizedEllipticContraction, + u: &DVector, + quadrature_table: &impl QuadratureTable, +) -> CooMatrix +where + T: RealField, + Connectivity: ElementConnectivity>::GeometryDim>, + Connectivity::GeometryDim: DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, +{ + let ndof = vertices.len() * SolutionDim::dim(); + let mut coo = CooMatrix::new(ndof, ndof); + assemble_generalized_stiffness_into(&mut coo, vertices, connectivity, contraction, u, quadrature_table); + coo +} + +pub fn assemble_generalized_stiffness_par( + vertices: &[Point], + connectivity: &[Connectivity], + contraction: &(impl Sync + GeneralizedEllipticContraction), + u: &DVector, + quadrature_table: &(impl Sync + QuadratureTable), +) -> CooMatrix +where + T: RealField, + Connectivity: Sync + ElementConnectivity>::GeometryDim>, + Connectivity::GeometryDim: DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, + >::Buffer: Sync, +{ + assemble_transformed_generalized_stiffness_par( + vertices, + connectivity, + contraction, + u, + quadrature_table, + &NoTransformation, + ) +} + +pub fn assemble_transformed_generalized_stiffness_par( + vertices: &[Point], + connectivity: &[Connectivity], + contraction: &(impl Sync + GeneralizedEllipticContraction), + u: &DVector, + quadrature_table: &(impl Sync + QuadratureTable), + transformation: &(impl ?Sized + Sync + ElementMatrixTransformation), +) -> CooMatrix +where + T: RealField, + Connectivity: Sync + ElementConnectivity>::GeometryDim>, + Connectivity::GeometryDim: DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, + >::Buffer: Sync, +{ + let ndof = vertices.len() * SolutionDim::dim(); + connectivity + .par_iter() + .enumerate() + .fold( + || CooMatrix::new(ndof, ndof), + |mut coo, (i, connectivity)| { + let element = connectivity.element(vertices).expect( + "All vertices of element are assumed to be in bounds.\ + TODO: Ensure this upon construction of basis?", + ); + let u_element = connectivity.element_variables(u); + let mut a_element = assemble_generalized_element_stiffness( + &element, + contraction, + &u_element, + &quadrature_table.quadrature_for_element(i), + ); + + let a_rows = a_element.nrows(); + let a_cols = a_element.ncols(); + let mut a_dynamic_slice_mut = + coerce_col_major_slice_mut(&mut a_element, Dynamic::new(a_rows), Dynamic::new(a_cols)); + transformation.transform_element_matrix(&mut a_dynamic_slice_mut); + distribute_local_to_global(&mut coo, connectivity.vertex_indices(), &a_element); + coo + }, + ) + .reduce_with(|mut coo1, coo2| { + // TODO: As a slight optimization, we might consider making sure that the + // matrix with the largest number of nonzeros is on the left-hand side + coo1 += &coo2; + coo1 + }) + .unwrap_or_else(|| CooMatrix::new(ndof, ndof)) +} + +pub fn assemble_generalized_mass_into( + coo: &mut CooMatrix, + vertices: &[Point], + connectivity: &[Connectivity], + // TODO: Generalize density somehow? Attach properties to quadrature points? + density: T, + quadrature_table: &Table, +) where + T: RealField, + Connectivity: ElementConnectivity>::GeometryDim>, + Connectivity::GeometryDim: DimMin, + Table: QuadratureTable, + SolutionDim: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, +{ + for (i, connectivity) in connectivity.iter().enumerate() { + let element = connectivity.element(vertices).expect( + "All vertices of element are assumed to be in bounds.\ + TODO: Ensure this upon construction of basis?", + ); + let m_element = + assemble_generalized_element_mass(&element, density, &quadrature_table.quadrature_for_element(i)); + + distribute_local_to_global(coo, connectivity.vertex_indices(), &m_element); + } +} + +pub fn assemble_generalized_mass( + vertices: &[Point], + connectivity: &[Connectivity], + // TODO: Generalize density somehow? Attach properties to quadrature points? + density: T, + quadrature_table: &Table, +) -> CooMatrix +where + T: RealField, + Connectivity: ElementConnectivity>::GeometryDim>, + Connectivity::GeometryDim: DimMin, + Table: QuadratureTable, + SolutionDim: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, +{ + let ndof = vertices.len() * SolutionDim::dim(); + let mut coo = CooMatrix::new(ndof, ndof); + assemble_generalized_mass_into(&mut coo, vertices, connectivity, density, quadrature_table); + coo +} + +pub fn color_nodes(connectivity: &[C]) -> Vec { + let mut nested = NestedVec::new(); + + for conn in connectivity { + nested.push(conn.vertex_indices()); + } + + sequential_greedy_coloring(&nested) +} + +fn distribute_local_to_global( + coo: &mut CooMatrix, + global_indices: &[usize], + element_matrix: &SquareMatrix, +) where + T: ComplexField, + D: Dim, + S: Storage, +{ + assert_eq!( + element_matrix.nrows() % global_indices.len(), + 0, + "Element matrix must have number of rows/cols divisible by \ + number of nodes in element." + ); + let dim = element_matrix.nrows() / global_indices.len(); + // Distribute values from element matrix to global matrix in the form of triplets + for (i_local, i_global) in global_indices.iter().enumerate() { + for (j_local, j_global) in global_indices.iter().enumerate() { + for i in 0..dim { + for j in 0..dim { + coo.push( + dim * i_global + i, + dim * j_global + j, + *element_matrix.index((dim * i_local + i, dim * j_local + j)), + ); + } + } + } + } +} + +pub fn distribute_local_to_global_vector( + mut vector: DVectorSliceMut, + global_indices: &[usize], + element_vectors: &Matrix, +) where + T: ComplexField, + Dimension: Dim, + Nodes: Dim, + S: Storage, +{ + assert_eq!( + element_vectors.ncols(), + global_indices.len(), + "Number of elements vectors must be same as number of nodes in element." + ); + let dim = element_vectors.nrows(); + for (i_local, i_global) in global_indices.iter().enumerate() { + for i in 0..dim { + vector[dim * i_global + i] += element_vectors[dim * i_local + i]; + } + } +} + +// TODO: Write tests for distribute_local_to_global diff --git a/fenris/src/cg.rs b/fenris/src/cg.rs new file mode 100644 index 0000000..4e5899d --- /dev/null +++ b/fenris/src/cg.rs @@ -0,0 +1,483 @@ +use crate::sparse::spmv_csr; +use crate::CsrMatrix; +use core::fmt; +use nalgebra::base::constraint::AreMultipliable; +use nalgebra::constraint::{DimEq, ShapeConstraint}; +use nalgebra::storage::Storage; +use nalgebra::{ + ClosedAdd, ClosedMul, DVector, DVectorSlice, DVectorSliceMut, Dim, Dynamic, Matrix, RealField, Scalar, U1, +}; +use num::{One, Zero}; +use std::error::Error; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; + +pub trait LinearOperator { + fn apply(&self, y: DVectorSliceMut, x: DVectorSlice) -> Result<(), Box>; +} + +impl<'a, T, A> LinearOperator for &'a A +where + T: Scalar, + A: ?Sized + LinearOperator, +{ + fn apply(&self, y: DVectorSliceMut, x: DVectorSlice) -> Result<(), Box> { + >::apply(self, y, x) + } +} + +impl LinearOperator for Matrix +where + T: Scalar + One + Zero + ClosedMul + ClosedAdd, + R: Dim, + C: Dim, + S: Storage, + ShapeConstraint: DimEq + DimEq + AreMultipliable, +{ + fn apply(&self, mut y: DVectorSliceMut, x: DVectorSlice) -> Result<(), Box> { + y.gemv(T::one(), self, &x, T::zero()); + Ok(()) + } +} + +impl LinearOperator for CsrMatrix +where + T: Scalar + Zero + One + ClosedMul + ClosedAdd, +{ + fn apply(&self, mut y: DVectorSliceMut, x: DVectorSlice) -> Result<(), Box> { + spmv_csr(T::zero(), &mut y, T::one(), self, &x); + Ok(()) + } +} + +pub struct IdentityOperator; + +impl LinearOperator for IdentityOperator { + fn apply(&self, mut y: DVectorSliceMut, x: DVectorSlice) -> Result<(), Box> { + y.copy_from(&x); + Ok(()) + } +} + +pub trait CgStoppingCriterion { + /// Called by CG at the start of a new solve. + fn reset(&self, _a: &dyn LinearOperator, _x: DVectorSlice, _b: DVectorSlice) {} + + fn has_converged( + &self, + a: &dyn LinearOperator, + x: DVectorSlice, + b: DVectorSlice, + b_norm: T, + iteration: usize, + approx_residual: DVectorSlice, + ) -> Result; +} + +/// Relative residual tolerance ||r|| <= tol * ||b||. +/// +/// Note that we use the *approximate* residual given by Conjugate-Gradient. For ill-conditioned +/// problems, it is possible that CG's residual converges, but the real residual does not. +/// However, in these cases, it is often the case that CG in any case is unable to obtain +/// a more accurate solution, and a better preconditioner would be required if a high-resolution +/// solution is desired. +#[derive(Debug)] +pub struct RelativeResidualCriterion { + tol: T, +} + +impl RelativeResidualCriterion { + pub fn new(tol: T) -> Self { + Self { tol } + } +} + +impl Default for RelativeResidualCriterion { + fn default() -> Self { + Self::new(1e-8) + } +} + +impl Default for RelativeResidualCriterion { + fn default() -> Self { + Self::new(1e-4) + } +} + +impl CgStoppingCriterion for RelativeResidualCriterion +where + T: RealField, +{ + fn has_converged( + &self, + _a: &dyn LinearOperator, + _x: DVectorSlice, + _b: DVectorSlice, + b_norm: T, + _iteration: usize, + approx_residual: DVectorSlice, + ) -> Result { + let r_approx_norm = approx_residual.norm(); + let converged = r_approx_norm <= self.tol * b_norm; + Ok(converged) + } +} + +#[derive(Debug, Clone)] +#[allow(non_snake_case)] +pub struct CgWorkspace { + r: DVector, + z: DVector, + p: DVector, + Ap: DVector, +} + +#[allow(non_snake_case)] +struct Buffers<'a, T: Scalar> { + r: &'a mut DVector, + z: &'a mut DVector, + p: &'a mut DVector, + Ap: &'a mut DVector, +} + +impl Default for CgWorkspace { + fn default() -> Self { + Self { + r: DVector::zeros(0), + z: DVector::zeros(0), + p: DVector::zeros(0), + Ap: DVector::zeros(0), + } + } +} + +impl CgWorkspace { + fn prepare_buffers(&mut self, dim: usize) -> Buffers { + self.r.resize_vertically_mut(dim, T::zero()); + self.z.resize_vertically_mut(dim, T::zero()); + self.p.resize_vertically_mut(dim, T::zero()); + self.Ap.resize_vertically_mut(dim, T::zero()); + Buffers { + r: &mut self.r, + z: &mut self.z, + p: &mut self.p, + Ap: &mut self.Ap, + } + } +} + +#[derive(Debug)] +enum OwnedOrMutRef<'a, T> { + Owned(T), + MutRef(&'a mut T), +} + +impl<'a, T> Deref for OwnedOrMutRef<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + match self { + Self::Owned(owned) => &owned, + Self::MutRef(mutref) => &*mutref, + } + } +} + +impl<'a, T> DerefMut for OwnedOrMutRef<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + match self { + Self::Owned(owned) => owned, + Self::MutRef(mutref) => mutref, + } + } +} + +#[derive(Debug)] +pub struct ConjugateGradient<'a, T, A, P, Criterion> +where + T: Scalar, +{ + workspace: OwnedOrMutRef<'a, CgWorkspace>, + operator: A, + preconditioner: P, + stopping_criterion: Criterion, + max_iter: Option, +} + +impl<'a, T: Scalar + Zero> ConjugateGradient<'a, T, (), IdentityOperator, ()> { + pub fn new() -> Self { + Self { + workspace: OwnedOrMutRef::Owned(CgWorkspace::default()), + operator: (), + preconditioner: IdentityOperator, + stopping_criterion: (), + max_iter: None, + } + } +} + +impl<'a, T: Scalar> ConjugateGradient<'a, T, (), IdentityOperator, ()> { + pub fn with_workspace(workspace: &'a mut CgWorkspace) -> Self { + Self { + workspace: OwnedOrMutRef::MutRef(workspace), + operator: (), + preconditioner: IdentityOperator, + stopping_criterion: (), + max_iter: None, + } + } +} + +impl<'a, T: Scalar, P, Criterion> ConjugateGradient<'a, T, (), P, Criterion> { + pub fn with_operator(self, operator: A) -> ConjugateGradient<'a, T, A, P, Criterion> { + ConjugateGradient { + workspace: self.workspace, + operator, + preconditioner: self.preconditioner, + stopping_criterion: self.stopping_criterion, + max_iter: self.max_iter, + } + } +} + +impl<'a, T: Scalar, A, P, Criterion> ConjugateGradient<'a, T, A, P, Criterion> { + pub fn with_preconditioner(self, preconditioner: P2) -> ConjugateGradient<'a, T, A, P2, Criterion> { + ConjugateGradient { + workspace: self.workspace, + operator: self.operator, + preconditioner, + stopping_criterion: self.stopping_criterion, + max_iter: self.max_iter, + } + } + + pub fn with_max_iter(self, max_iter: usize) -> Self { + Self { + max_iter: Some(max_iter), + ..self + } + } +} + +impl<'a, T: Scalar, A, P> ConjugateGradient<'a, T, A, P, ()> { + pub fn with_stopping_criterion( + self, + stopping_criterion: Criterion, + ) -> ConjugateGradient<'a, T, A, P, Criterion> { + ConjugateGradient { + workspace: self.workspace, + operator: self.operator, + preconditioner: self.preconditioner, + stopping_criterion, + max_iter: self.max_iter, + } + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub enum SolveErrorKind { + OperatorError(Box), + PreconditionerError(Box), + StoppingCriterionError(Box), + IndefiniteOperator, + IndefinitePreconditioner, + MaxIterationsReached { max_iter: usize }, +} + +impl fmt::Display for SolveErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::OperatorError(err) => { + write!(f, "Error applying operator: ")?; + err.fmt(f) + } + Self::PreconditionerError(err) => { + write!(f, "Error applying preconditioner: ")?; + err.fmt(f) + } + Self::StoppingCriterionError(err) => { + write!(f, "Error evaluating stopping criterion: ")?; + err.fmt(f) + } + Self::IndefiniteOperator => { + write!(f, "Operator appears to be indefinite: ") + } + Self::IndefinitePreconditioner => { + write!(f, "Indefinite preconditioner: ") + } + Self::MaxIterationsReached { max_iter } => { + write!(f, "Max iterations ({}) reached.", max_iter) + } + } + } +} + +#[non_exhaustive] +#[derive(Debug)] +pub struct SolveError { + pub output: CgOutput, + pub kind: SolveErrorKind, +} + +impl SolveError { + fn new(output: CgOutput, kind: SolveErrorKind) -> Self { + Self { output, kind } + } +} + +impl fmt::Display for SolveError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "CG solve failed after {}", self.output.num_iterations)?; + write!(f, "Error: {}", self.kind) + } +} + +impl std::error::Error for SolveError {} + +/// y = Ax +fn apply_operator<'a, T, A>( + y: impl Into>, + a: &'a A, + x: impl Into>, +) -> Result<(), Box> +where + T: Scalar, + A: LinearOperator, +{ + a.apply(y.into(), x.into()) +} + +#[non_exhaustive] +#[derive(Debug, Clone)] +pub struct CgOutput { + /// Number of iterations of the solver. + /// + /// Corresponds to the number of updates made to the (initial) solution vector, + pub num_iterations: usize, + marker: PhantomData, +} + +impl<'a, T, A, P, Criterion> ConjugateGradient<'a, T, A, P, Criterion> +where + T: RealField, + A: LinearOperator, + P: LinearOperator, + Criterion: CgStoppingCriterion, +{ + pub fn solve_with_guess<'b>( + &mut self, + b: impl Into>, + x: impl Into>, + ) -> Result, SolveError> { + self.solve_with_guess_(b.into(), x.into()) + } + + #[allow(non_snake_case)] + fn solve_with_guess_( + &mut self, + b: DVectorSlice, + mut x: DVectorSliceMut, + ) -> Result, SolveError> { + use SolveErrorKind::*; + assert_eq!(b.len(), x.len()); + + let mut output = CgOutput { + num_iterations: 0, + marker: PhantomData, + }; + + let Buffers { r, z, p, Ap } = self.workspace.prepare_buffers(x.len()); + + // r = b - Ax + if let Err(err) = apply_operator(&mut *r, &self.operator, &x) { + return Err(SolveError::new(output, OperatorError(err))); + } + r.zip_apply(&b, |Ax_i, b_i| b_i - Ax_i); + + // z = Pr + if let Err(err) = apply_operator(&mut *z, &self.preconditioner, &*r) { + return Err(SolveError::new(output, PreconditionerError(err))); + } + + // p = z + p.copy_from(&z); + + let mut zTr = z.dot(r); + let mut pAp; + + let b_norm = b.norm(); + + if b_norm == T::zero() { + x.fill(T::zero()); + return Ok(output); + } + + loop { + // TODO: Can we simplify this monstronsity? + let convergence = self.stopping_criterion.has_converged( + &self.operator, + (&x).into(), + (&b).into(), + b_norm, + output.num_iterations, + (&*r).into(), + ); + + let has_converged = match convergence { + Ok(converged) => converged, + Err(error_kind) => return Err(SolveError::new(output, error_kind)), + }; + + if has_converged { + break; + } else if let Some(max_iter) = self.max_iter { + if output.num_iterations >= max_iter { + return Err(SolveError::new(output, MaxIterationsReached { max_iter })); + } + } + + // Ap = A * p + if let Err(err) = apply_operator(&mut *Ap, &self.operator, &*p) { + return Err(SolveError::new(output, OperatorError(err))); + } + pAp = p.dot(&Ap); + + if pAp <= T::zero() { + return Err(SolveError { + output, + kind: SolveErrorKind::IndefiniteOperator, + }); + } + if zTr <= T::zero() { + return Err(SolveError { + output, + kind: SolveErrorKind::IndefinitePreconditioner, + }); + } + + let alpha = zTr / pAp; + // x <- x + alpha * p + x.zip_apply(&*p, |x_i, p_i| x_i + alpha * p_i); + // r <- r - alpha * Ap + r.zip_apply(&*Ap, |r_i, Ap_i| r_i - alpha * Ap_i); + + // Number of iterations corresponds to number of updates to the x vector + output.num_iterations += 1; + + // z <- P r + if let Err(err) = apply_operator(&mut *z, &self.preconditioner, &*r) { + return Err(SolveError::new(output, PreconditionerError(err))); + } + let zTr_next = z.dot(&*r); + let beta = zTr_next / zTr; + + // p <- z + beta * p + p.zip_apply(&*z, |p_i, z_i| z_i + beta * p_i); + + zTr = zTr_next; + } + + Ok(output) + } +} diff --git a/fenris/src/connectivity.rs b/fenris/src/connectivity.rs new file mode 100644 index 0000000..a8f1eb8 --- /dev/null +++ b/fenris/src/connectivity.rs @@ -0,0 +1,1019 @@ +use crate::geometry::{Hexahedron, LineSegment2d, Quad2d, Tetrahedron, Triangle, Triangle2d, Triangle3d}; +use itertools::izip; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, DimName, Point, Point2, Point3, RealField, Scalar, U2, U3}; +use serde::{Deserialize, Serialize}; +use std::ops::{Deref, DerefMut}; + +/// Represents the type of the faces for a given cell connectivity. +pub type CellFace = <::FaceConnectivity as CellConnectivity>::Cell; + +pub trait Connectivity: Clone { + type FaceConnectivity: Connectivity; + + fn num_faces(&self) -> usize; + fn get_face_connectivity(&self, index: usize) -> Option; + + fn vertex_indices(&self) -> &[usize]; +} + +pub trait ConnectivityMut: Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize]; +} + +pub trait CellConnectivity: Connectivity +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, +{ + type Cell; + + /// Legacy method + /// + /// TODO: Remove in favor of using `num_faces` and `get_face_Connectivity` + fn for_each_face(&self, mut f: F) + where + F: FnMut(Self::FaceConnectivity), + { + let num_faces = self.num_faces(); + for i in 0..num_faces { + let face = self + .get_face_connectivity(i) + .expect("Since index is in bounds, connectivity must exist."); + f(face) + } + } + + fn cell(&self, vertices: &[Point]) -> Option; +} + +/// Connectivity for a two-dimensional Quad9 element. +/// +/// A Quad9 element has a quadrilateral geometry, with 9 nodes evenly distributed across +/// the surface of the reference element [-1, 1]^2. +/// +/// Note that the element is not completely isoparametric: The element itself is assumed to have +/// straight faces, i.e. the same as a bilinear quad element. +/// +/// The schematic below demonstrates the node numbering. +/// +/// ```text +/// 3____6____2 +/// | | +/// 7 8 5 +/// | | +/// 0____4____1 +/// ``` +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Quad9d2Connectivity(pub [usize; 9]); + +impl<'a> From<&'a Quad9d2Connectivity> for Quad4d2Connectivity { + fn from(quad9: &'a Quad9d2Connectivity) -> Self { + let Quad9d2Connectivity(indices) = quad9; + Quad4d2Connectivity([indices[0], indices[1], indices[2], indices[3]]) + } +} + +impl Deref for Quad9d2Connectivity { + type Target = [usize]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Segment2d2Connectivity(pub [usize; 2]); + +impl Connectivity for Segment2d2Connectivity { + type FaceConnectivity = (); + + fn num_faces(&self) -> usize { + 0 + } + + fn get_face_connectivity(&self, _index: usize) -> Option { + None + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Segment2d2Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +impl CellConnectivity for Segment2d2Connectivity +where + T: Scalar, +{ + type Cell = LineSegment2d; + + fn cell(&self, vertices: &[Point2]) -> Option { + let a = vertices.get(self.0[0]).cloned()?; + let b = vertices.get(self.0[1]).cloned()?; + Some(LineSegment2d::new(a, b)) + } +} + +/// Connectivity for a two-dimensional Quad4 element. +/// +/// A Quad4 element has a quadrilateral geometry, with 4 nodes distributed across +/// the corners of the reference element [-1, 1]^2. +/// +/// The schematic below demonstrates the node numbering. +/// +/// ```text +/// 3_________2 +/// | | +/// | | +/// | | +/// 0_________1 +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Quad4d2Connectivity(pub [usize; 4]); + +impl Deref for Quad4d2Connectivity { + type Target = [usize; 4]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Quad4d2Connectivity { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Connectivity for Quad4d2Connectivity { + type FaceConnectivity = Segment2d2Connectivity; + + fn num_faces(&self) -> usize { + 4 + } + + fn get_face_connectivity(&self, index: usize) -> Option { + let idx = &self.0; + if index < 4 { + Some(Segment2d2Connectivity([idx[index], idx[(index + 1) % 4]])) + } else { + None + } + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Quad4d2Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +impl CellConnectivity for Quad4d2Connectivity +where + T: Scalar, +{ + type Cell = Quad2d; + + fn cell(&self, vertices: &[Point2]) -> Option { + Some(Quad2d([ + vertices.get(self.0[0]).cloned()?, + vertices.get(self.0[1]).cloned()?, + vertices.get(self.0[2]).cloned()?, + vertices.get(self.0[3]).cloned()?, + ])) + } +} + +/// Connectivity for a two-dimensional Tri3 element. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] +pub struct Tri3d2Connectivity(pub [usize; 3]); + +impl Connectivity for Tri3d2Connectivity { + type FaceConnectivity = Segment2d2Connectivity; + + fn num_faces(&self) -> usize { + 3 + } + + fn get_face_connectivity(&self, index: usize) -> Option { + let idx = &self.0; + if index < 3 { + Some(Segment2d2Connectivity([idx[index], idx[(index + 1) % 3]])) + } else { + None + } + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Tri3d2Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +impl CellConnectivity for Tri3d2Connectivity +where + T: Scalar, +{ + type Cell = Triangle2d; + + fn cell(&self, vertices: &[Point2]) -> Option { + Some(Triangle([ + vertices.get(self.0[0]).cloned()?, + vertices.get(self.0[1]).cloned()?, + vertices.get(self.0[2]).cloned()?, + ])) + } +} + +impl Deref for Tri3d2Connectivity { + type Target = [usize; 3]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Tri3d2Connectivity { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// Connectivity for a two-dimensional Tri6 element. +//// +/// +/// The schematic below demonstrates the node numbering. +/// +/// ```text +/// 2 +/// |`\ +/// | `\ +/// 5 `4 +/// | `\ +/// | `\ +/// 0-----3----1 +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] +pub struct Tri6d2Connectivity(pub [usize; 6]); + +impl<'a> From<&'a Tri6d2Connectivity> for Tri3d2Connectivity { + fn from(tri6: &'a Tri6d2Connectivity) -> Self { + let Tri6d2Connectivity(indices) = tri6; + Tri3d2Connectivity([indices[0], indices[1], indices[2]]) + } +} + +impl Deref for Tri6d2Connectivity { + type Target = [usize; 6]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Connectivity for Tri6d2Connectivity { + type FaceConnectivity = Segment3d2Connectivity; + + fn num_faces(&self) -> usize { + 3 + } + + fn get_face_connectivity(&self, index: usize) -> Option { + let idx = &self.0; + if index < 3 { + Some(Segment3d2Connectivity([ + idx[index], + idx[index + 3], + idx[(index + 1) % 3], + ])) + } else { + None + } + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Tri6d2Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +impl CellConnectivity for Tri6d2Connectivity +where + T: Scalar, +{ + type Cell = Triangle2d; + + fn cell(&self, vertices: &[Point2]) -> Option { + Some(Triangle([ + vertices.get(self.0[0]).cloned()?, + vertices.get(self.0[1]).cloned()?, + vertices.get(self.0[2]).cloned()?, + ])) + } +} + +/// Connectivity for a 2D segment element of polynomial degree 2. +/// +/// This connectivity is used e.g. to represent the faces of a Quad9 element. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct Segment3d2Connectivity(pub [usize; 3]); + +impl Connectivity for Segment3d2Connectivity { + type FaceConnectivity = (); + + fn num_faces(&self) -> usize { + 0 + } + + fn get_face_connectivity(&self, _index: usize) -> Option { + None + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Segment3d2Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +impl Connectivity for Quad9d2Connectivity { + type FaceConnectivity = Segment3d2Connectivity; + + fn num_faces(&self) -> usize { + 4 + } + + fn get_face_connectivity(&self, index: usize) -> Option { + let v = &self.0; + match index { + 0 => Some(Segment3d2Connectivity([v[0], v[4], v[1]])), + 1 => Some(Segment3d2Connectivity([v[1], v[5], v[2]])), + 2 => Some(Segment3d2Connectivity([v[2], v[6], v[3]])), + 3 => Some(Segment3d2Connectivity([v[3], v[7], v[0]])), + _ => None, + } + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Quad9d2Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +/// TODO: Move this somewhere else. Also figure out a better way to deal with Cell/Element +/// distinctions +impl CellConnectivity for Quad9d2Connectivity +where + T: Scalar, +{ + type Cell = >::Cell; + + fn cell(&self, vertices: &[Point2]) -> Option { + let quad4 = Quad4d2Connectivity::from(self); + quad4.cell(vertices) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Quad8d3Connectivity(pub [usize; 8]); + +impl Connectivity for Quad8d3Connectivity { + type FaceConnectivity = Segment3d3Connectivity; + + fn num_faces(&self) -> usize { + 4 + } + + fn get_face_connectivity(&self, index: usize) -> Option { + let v = &self.0; + let segment = |a, b, c| Some(Segment3d3Connectivity([v[a], v[b], v[c]])); + + match index { + // TODO: We need to fix this later. We're currently using a kind of bogus + // ordering for segments + 0 => segment(0, 4, 1), + 1 => segment(1, 5, 2), + 2 => segment(2, 6, 3), + 3 => segment(3, 7, 0), + _ => None, + } + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Quad8d3Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Quad9d3Connectivity(pub [usize; 9]); + +impl Connectivity for Quad9d3Connectivity { + type FaceConnectivity = Segment3d3Connectivity; + + fn num_faces(&self) -> usize { + 4 + } + + fn get_face_connectivity(&self, index: usize) -> Option { + let v = &self.0; + let segment = |a, b, c| Some(Segment3d3Connectivity([v[a], v[b], v[c]])); + + match index { + // TODO: We need to fix this later. We're currently using a kind of bogus + // ordering for segments + 0 => segment(0, 4, 1), + 1 => segment(1, 5, 2), + 2 => segment(2, 6, 3), + 3 => segment(3, 7, 0), + _ => None, + } + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Quad9d3Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Tet4Connectivity(pub [usize; 4]); + +impl Connectivity for Tet4Connectivity { + type FaceConnectivity = Tri3d3Connectivity; + + fn num_faces(&self) -> usize { + 4 + } + + fn get_face_connectivity(&self, index: usize) -> Option { + let v = &self.0; + // Note: need to carefully choose faces such that their normal point outwards, + // otherwise extracted surface meshes have normals the wrong way around + match index { + 0 => Some(Tri3d3Connectivity([v[0], v[2], v[1]])), + 1 => Some(Tri3d3Connectivity([v[0], v[1], v[3]])), + 2 => Some(Tri3d3Connectivity([v[1], v[2], v[3]])), + 3 => Some(Tri3d3Connectivity([v[0], v[3], v[2]])), + _ => None, + } + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Tet4Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +impl CellConnectivity for Tet4Connectivity +where + T: RealField, +{ + type Cell = Tetrahedron; + + fn cell(&self, vertices: &[Point3]) -> Option { + let mut tet_vertices = [Point3::origin(); 4]; + for (tet_v, idx) in izip!(&mut tet_vertices, &self.0) { + *tet_v = vertices[*idx]; + } + Some(Tetrahedron::from_vertices(tet_vertices)) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct Quad4d3Connectivity(pub [usize; 4]); + +impl Connectivity for Quad4d3Connectivity { + type FaceConnectivity = Segment2d3Connectivity; + + fn num_faces(&self) -> usize { + 4 + } + + fn get_face_connectivity(&self, index: usize) -> Option { + let v = &self.0; + + let segment = |a, b| Some(Segment2d3Connectivity([v[a], v[b]])); + + match index { + 0 => segment(0, 1), + 1 => segment(1, 2), + 2 => segment(2, 3), + 3 => segment(3, 0), + _ => None, + } + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Quad4d3Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Hex8Connectivity(pub [usize; 8]); + +impl Connectivity for Hex8Connectivity { + type FaceConnectivity = Quad4d3Connectivity; + + fn num_faces(&self) -> usize { + 6 + } + + fn get_face_connectivity(&self, index: usize) -> Option { + let v = &self.0; + + let quad = |i, j, k, l| Some(Quad4d3Connectivity([v[i], v[j], v[k], v[l]])); + + // Must choose faces carefully so that they point towards the *exterior*, + // in order to get proper surface normals for the boundary + // Note: This is just the oppositely oriented choices of the current (at time of writing) + // implementation of ConvexPolyhedron for Hexahedron + match index { + 0 => quad(3, 2, 1, 0), + 1 => quad(0, 1, 5, 4), + 2 => quad(1, 2, 6, 5), + 3 => quad(2, 3, 7, 6), + 4 => quad(4, 7, 3, 0), + 5 => quad(5, 6, 7, 4), + _ => None, + } + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Hex8Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +impl CellConnectivity for Hex8Connectivity +where + T: RealField, +{ + type Cell = Hexahedron; + + fn cell(&self, vertices: &[Point3]) -> Option { + let mut hex_vertices = [Point3::origin(); 8]; + for (v, idx) in izip!(&mut hex_vertices, &self.0) { + *v = vertices[*idx]; + } + Some(Hexahedron::from_vertices(hex_vertices)) + } +} + +/// Connectivity for a 3D tri-quadratic Hex element. +/// +/// The node ordering is the same as defined by gmsh, see +/// [http://gmsh.info/doc/texinfo/gmsh.html#Low-order-elements] for more information. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Hex27Connectivity(pub [usize; 27]); + +impl Connectivity for Hex27Connectivity { + type FaceConnectivity = Quad9d3Connectivity; + + fn num_faces(&self) -> usize { + 6 + } + + fn get_face_connectivity(&self, index: usize) -> Option { + let v = &self.0; + + // The macro just takes care of the boilerplate from mapping local + // indices to global indices in the resulting Quad9d3Connectivity + macro_rules! quad9 { + ($($idx:expr),+) => { Some(Quad9d3Connectivity([$(v[$idx],)+])) } + } + + // Must choose faces carefully so that they point towards the *exterior*, + // in order to get proper surface normals for the boundary + match index { + 0 => quad9!(0, 3, 2, 1, 9, 13, 11, 8, 20), + 1 => quad9!(0, 1, 5, 4, 8, 12, 16, 10, 21), + 2 => quad9!(1, 2, 6, 5, 11, 14, 18, 12, 23), + 3 => quad9!(2, 3, 7, 6, 13, 15, 19, 14, 24), + 4 => quad9!(0, 4, 7, 3, 10, 17, 15, 9, 22), + 5 => quad9!(4, 5, 6, 7, 16, 18, 19, 17, 25), + _ => None, + } + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Hex27Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +impl CellConnectivity for Hex27Connectivity +where + T: RealField, +{ + type Cell = Hexahedron; + + fn cell(&self, vertices: &[Point3]) -> Option { + let mut hex_vertices = [Point3::origin(); 8]; + // The first 8 vertices are the same as the linear hex element + for (v, idx) in izip!(&mut hex_vertices, &self.0) { + *v = vertices[*idx]; + } + Some(Hexahedron::from_vertices(hex_vertices)) + } +} + +/// Connectivity for a 3D 20-node Hex element. +/// +/// The node ordering is the same as defined by gmsh, see +/// [http://gmsh.info/doc/texinfo/gmsh.html#Low-order-elements] for more information. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Hex20Connectivity(pub [usize; 20]); + +impl Connectivity for Hex20Connectivity { + // TODO: Implement FaceConnectivity for Hex27 + type FaceConnectivity = Quad8d3Connectivity; + + fn num_faces(&self) -> usize { + 6 + } + + fn get_face_connectivity(&self, index: usize) -> Option { + let v = &self.0; + + // The macro just takes care of the boilerplate from mapping local + // indices to global indices in the resulting Quad9d3Connectivity + macro_rules! quad8 { + ($($idx:expr),+) => { Some(Quad8d3Connectivity([$(v[$idx],)+])) } + } + + // Must choose faces carefully so that they point towards the *exterior*, + // in order to get proper surface normals for the boundary + + // Note: This is identical to Hex27, except for the lack of the midpoint on each face + match index { + 0 => quad8!(0, 3, 2, 1, 9, 13, 11, 8), + 1 => quad8!(0, 1, 5, 4, 8, 12, 16, 10), + 2 => quad8!(1, 2, 6, 5, 11, 14, 18, 12), + 3 => quad8!(2, 3, 7, 6, 13, 15, 19, 14), + 4 => quad8!(0, 4, 7, 3, 10, 17, 15, 9), + 5 => quad8!(4, 5, 6, 7, 16, 18, 19, 17), + _ => None, + } + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Hex20Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +impl CellConnectivity for Hex20Connectivity +where + T: RealField, +{ + type Cell = Hexahedron; + + fn cell(&self, vertices: &[Point3]) -> Option { + let mut hex_vertices = [Point3::origin(); 8]; + // The first 8 vertices are the same as the linear hex element + for (v, idx) in izip!(&mut hex_vertices, &self.0) { + *v = vertices[*idx]; + } + Some(Hexahedron::from_vertices(hex_vertices)) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] +pub struct Tri3d3Connectivity(pub [usize; 3]); + +impl Connectivity for Tri3d3Connectivity { + type FaceConnectivity = Segment2d3Connectivity; + + fn num_faces(&self) -> usize { + 3 + } + + fn get_face_connectivity(&self, index: usize) -> Option { + let segment = |i, j| Some(Segment2d3Connectivity([self.0[i], self.0[j]])); + match index { + 0 => segment(0, 1), + 1 => segment(1, 2), + 2 => segment(2, 0), + _ => None, + } + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Tri3d3Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +impl CellConnectivity for Tri3d3Connectivity +where + T: Scalar, +{ + type Cell = Triangle3d; + + fn cell(&self, vertices: &[Point3]) -> Option { + Some(Triangle([ + vertices.get(self.0[0]).cloned()?, + vertices.get(self.0[1]).cloned()?, + vertices.get(self.0[2]).cloned()?, + ])) + } +} + +impl Deref for Tri3d3Connectivity { + type Target = [usize; 3]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Tri3d3Connectivity { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] +pub struct Tri6d3Connectivity(pub [usize; 6]); + +impl Deref for Tri6d3Connectivity { + type Target = [usize; 6]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Connectivity for Tri6d3Connectivity { + type FaceConnectivity = Segment3d3Connectivity; + + fn num_faces(&self) -> usize { + 3 + } + + fn get_face_connectivity(&self, index: usize) -> Option { + let idx = &self.0; + if index < 3 { + Some(Segment3d3Connectivity([ + idx[index], + idx[index + 3], + idx[(index + 1) % 3], + ])) + } else { + None + } + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Tri6d3Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +impl CellConnectivity for Tri6d3Connectivity +where + T: Scalar, +{ + type Cell = Triangle3d; + + fn cell(&self, vertices: &[Point3]) -> Option { + Some(Triangle([ + vertices.get(self.0[0]).cloned()?, + vertices.get(self.0[1]).cloned()?, + vertices.get(self.0[2]).cloned()?, + ])) + } +} + +/// Connectivity for a 10-node tetrahedron element. +/// +/// See GMSH documentation for node ordering. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Tet10Connectivity(pub [usize; 10]); + +impl<'a> From<&'a Tet10Connectivity> for Tet4Connectivity { + fn from(tet10: &'a Tet10Connectivity) -> Self { + let Tet10Connectivity(indices) = tet10; + Tet4Connectivity([indices[0], indices[1], indices[2], indices[3]]) + } +} + +impl Connectivity for Tet10Connectivity { + type FaceConnectivity = Tri6d3Connectivity; + + fn num_faces(&self) -> usize { + 4 + } + + fn get_face_connectivity(&self, index: usize) -> Option { + let v = &self.0; + match index { + 0 => Some(Tri6d3Connectivity([v[0], v[2], v[1], v[6], v[5], v[4]])), + 1 => Some(Tri6d3Connectivity([v[0], v[1], v[3], v[4], v[9], v[7]])), + 2 => Some(Tri6d3Connectivity([v[1], v[2], v[3], v[5], v[8], v[9]])), + 3 => Some(Tri6d3Connectivity([v[0], v[3], v[2], v[7], v[8], v[6]])), + _ => None, + } + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Tet10Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +impl CellConnectivity for Tet10Connectivity +where + T: RealField, +{ + type Cell = Tetrahedron; + + fn cell(&self, vertices: &[Point3]) -> Option { + let mut tet4_v = [0, 0, 0, 0]; + tet4_v.clone_from_slice(&self.0[0..4]); + let tet4 = Tet4Connectivity(tet4_v); + tet4.cell(vertices) + } +} + +/// Connectivity for a 20-node tetrahedron element. +/// +/// See GMSH documentation for node ordering. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Tet20Connectivity(pub [usize; 20]); + +impl<'a> From<&'a Tet20Connectivity> for Tet4Connectivity { + fn from(tet20: &'a Tet20Connectivity) -> Self { + let Tet20Connectivity(indices) = tet20; + Tet4Connectivity([indices[0], indices[1], indices[2], indices[3]]) + } +} + +impl Connectivity for Tet20Connectivity { + // TODO: Connectivity? + type FaceConnectivity = (); + + fn num_faces(&self) -> usize { + 0 + } + + fn get_face_connectivity(&self, _index: usize) -> Option { + None + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Tet20Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +impl CellConnectivity for Tet20Connectivity +where + T: RealField, +{ + type Cell = Tetrahedron; + + fn cell(&self, vertices: &[Point3]) -> Option { + let mut tet4_v = [0, 0, 0, 0]; + tet4_v.clone_from_slice(&self.0[0..4]); + let tet4 = Tet4Connectivity(tet4_v); + tet4.cell(vertices) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Segment2d3Connectivity(pub [usize; 2]); + +impl Connectivity for Segment2d3Connectivity { + type FaceConnectivity = (); + + fn num_faces(&self) -> usize { + 0 + } + + fn get_face_connectivity(&self, _index: usize) -> Option { + None + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Segment2d3Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Segment3d3Connectivity(pub [usize; 3]); + +impl Connectivity for Segment3d3Connectivity { + type FaceConnectivity = (); + + fn num_faces(&self) -> usize { + 0 + } + + fn get_face_connectivity(&self, _index: usize) -> Option { + None + } + + fn vertex_indices(&self) -> &[usize] { + &self.0 + } +} + +impl ConnectivityMut for Segment3d3Connectivity { + fn vertex_indices_mut(&mut self) -> &mut [usize] { + &mut self.0 + } +} diff --git a/fenris/src/element.rs b/fenris/src/element.rs new file mode 100644 index 0000000..4a23061 --- /dev/null +++ b/fenris/src/element.rs @@ -0,0 +1,2349 @@ +use nalgebra::allocator::Allocator; +use nalgebra::{ + distance, DVectorSlice, DVectorSliceMut, DimName, Matrix1x6, Matrix2x6, Matrix3, Matrix3x4, Point2, Point3, + Vector1, Vector3, +}; +use nalgebra::{ + DefaultAllocator, DimMin, Matrix1x3, Matrix1x4, Matrix2, Matrix2x3, Matrix2x4, MatrixMN, RealField, Scalar, + Vector2, VectorN, U1, U10, U2, U20, U27, U3, U4, U6, U8, U9, +}; +use nalgebra::{Matrix3x2, Point}; + +use crate::connectivity::{ + Connectivity, Hex20Connectivity, Hex27Connectivity, Hex8Connectivity, Quad4d2Connectivity, Quad9d2Connectivity, + Tet10Connectivity, Tet20Connectivity, Tet4Connectivity, Tri3d2Connectivity, Tri3d3Connectivity, Tri6d2Connectivity, +}; +use crate::geometry::{ConcavePolygonError, ConvexPolygon, LineSegment2d, Quad2d, Triangle, Triangle2d, Triangle3d}; + +use itertools::Itertools; +use numeric_literals::replace_float_literals; +use std::convert::{TryFrom, TryInto}; +use std::fmt::Debug; + +use hamilton2::newton::NewtonSettings; +use num::Zero; +use std::error::Error; + +use crate::allocators::{FiniteElementAllocator, ReferenceFiniteElementAllocator, VolumeFiniteElementAllocator}; +use crate::connectivity::Segment2d2Connectivity; +use crate::space::FiniteElementSpace; + +pub trait ReferenceFiniteElement +where + T: Scalar, + DefaultAllocator: ReferenceFiniteElementAllocator, +{ + type ReferenceDim: DimName; + type NodalDim: DimName; + + /// Evaluates each basis function at the given reference coordinates. The result is given + /// in a row vector where each entry is the value of the corresponding basis function. + fn evaluate_basis(&self, reference_coords: &VectorN) -> MatrixMN; + + /// Given nodal weights, construct a matrix whose columns are the + /// gradients of each shape function in the element. + fn gradients( + &self, + reference_coords: &VectorN, + ) -> MatrixMN; +} + +pub trait FiniteElement: ReferenceFiniteElement +where + T: Scalar, + DefaultAllocator: FiniteElementAllocator, +{ + type GeometryDim: DimName; + + /// Compute the Jacobian of the transformation from the reference element to the given + /// element at the given reference coordinates. + fn reference_jacobian( + &self, + reference_coords: &VectorN, + ) -> MatrixMN; + + /// Maps reference coordinates to physical coordinates in the element. + fn map_reference_coords(&self, reference_coords: &VectorN) -> VectorN; + + /// The diameter of the finite element. + /// + /// The diameter of a finite element is defined as the largest distance between any two + /// points in the element, i.e. + /// h = min |x - y| for x, y in K + /// where K is the element and h is the diameter. + fn diameter(&self) -> T; +} + +/// TODO: Do we *really* need the Debug bound? +pub trait ElementConnectivity: Debug + Connectivity +where + T: Scalar, + DefaultAllocator: FiniteElementAllocator, +{ + type Element: FiniteElement< + T, + GeometryDim = Self::GeometryDim, + ReferenceDim = Self::ReferenceDim, + NodalDim = Self::NodalDim, + >; + type GeometryDim: DimName; + type ReferenceDim: DimName; + type NodalDim: DimName; + + fn element(&self, vertices: &[Point]) -> Option; + + /// TODO: Move this out of the trait itself? + fn element_variables<'a, SolutionDim>( + &self, + u_global: impl Into>, + ) -> MatrixMN + where + T: Zero, + SolutionDim: DimName, + DefaultAllocator: Allocator, + { + let u_global = u_global.into(); + let mut u = MatrixMN::::zeros(); + let indices = self.vertex_indices(); + let sol_dim = SolutionDim::dim(); + for (i_local, i_global) in indices.iter().enumerate() { + u.index_mut((.., i_local)) + .copy_from(&u_global.index((sol_dim * i_global..sol_dim * i_global + sol_dim, ..))); + } + u + } +} + +/// A finite element whose geometry dimension and reference dimension coincide. +pub trait VolumetricFiniteElement: FiniteElement>::GeometryDim> +where + T: Scalar, + DefaultAllocator: FiniteElementAllocator, +{ +} + +impl VolumetricFiniteElement for E +where + T: Scalar, + E: FiniteElement>::GeometryDim>, + DefaultAllocator: FiniteElementAllocator, +{ +} + +pub trait SurfaceFiniteElement: FiniteElement +where + T: Scalar, + DefaultAllocator: FiniteElementAllocator, +{ + /// Compute the normal at the point associated with the provided reference coordinate. + fn normal(&self, xi: &VectorN) -> VectorN; +} + +// TODO: Move these? +pub type ElementForSpace = ElementForConnectivity>::Connectivity>; +pub type ElementForConnectivity = >::Element; +pub type ConnectivityForSpace = >::Connectivity; + +pub type ConnectivityGeometryDim = >::GeometryDim; +pub type ConnectivityReferenceDim = >::ReferenceDim; +pub type ConnectivityNodalDim = >::NodalDim; + +pub type ElementGeometryDim = >::GeometryDim; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Quad4d2Element +where + T: Scalar, +{ + vertices: [Point2; 4], +} + +impl Quad4d2Element +where + T: Scalar, +{ + pub fn from_vertices(vertices: [Point2; 4]) -> Self { + Self { vertices } + } + + pub fn vertices(&self) -> &[Point2; 4] { + &self.vertices + } +} + +impl From> for Quad4d2Element +where + T: Scalar, +{ + fn from(quad: Quad2d) -> Self { + Self::from_vertices(quad.0) + } +} + +impl Quad4d2Element +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).unwrap())] + pub fn reference() -> Self { + Self::from_vertices([ + Point2::new(-1.0, -1.0), + Point2::new(1.0, -1.0), + Point2::new(1.0, 1.0), + Point2::new(-1.0, 1.0), + ]) + } +} + +impl TryFrom> for ConvexPolygon +where + T: RealField, +{ + type Error = ConcavePolygonError; + + fn try_from(value: Quad4d2Element) -> Result { + ConvexPolygon::try_from(Quad2d(value.vertices)) + } +} + +impl ElementConnectivity for Quad4d2Connectivity +where + T: RealField, +{ + type Element = Quad4d2Element; + type NodalDim = U4; + type ReferenceDim = U2; + type GeometryDim = U2; + + fn element(&self, vertices: &[Point2]) -> Option { + let Self(indices) = self; + let lookup_vertex = |local_index| vertices.get(indices[local_index]).cloned(); + + Some(Quad4d2Element::from_vertices([ + lookup_vertex(0)?, + lookup_vertex(1)?, + lookup_vertex(2)?, + lookup_vertex(3)?, + ])) + } +} + +impl ReferenceFiniteElement for Quad4d2Element +where + T: RealField, +{ + type NodalDim = U4; + type ReferenceDim = U2; + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn evaluate_basis(&self, xi: &Vector2) -> Matrix1x4 { + // We define the shape functions as N_{alpha, beta} evaluated at xi such that + // N_{alpha, beta}([alpha, beta]) = 1 + // with alpha, beta = 1 or -1 + let phi = |alpha, beta, xi: &Vector2| (1.0 + alpha * xi[0]) * (1.0 + beta * xi[1]) / 4.0; + Matrix1x4::from_row_slice(&[ + phi(-1.0, -1.0, xi), + phi( 1.0, -1.0, xi), + phi( 1.0, 1.0, xi), + phi(-1.0, 1.0, xi), + ]) + } + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn gradients(&self, xi: &Vector2) -> Matrix2x4 { + let phi_grad = |alpha, beta, xi: &Vector2| + Vector2::new( + alpha * (1.0 + beta * xi[1]) / 4.0, + beta * (1.0 + alpha * xi[0]) / 4.0, + ); + + Matrix2x4::from_columns(&[ + phi_grad(-1.0, -1.0, xi), + phi_grad( 1.0, -1.0, xi), + phi_grad( 1.0, 1.0, xi), + phi_grad(-1.0, 1.0, xi), + ]) + } +} + +impl FiniteElement for Quad4d2Element +where + T: RealField, +{ + type GeometryDim = U2; + + #[allow(non_snake_case)] + fn map_reference_coords(&self, xi: &Vector2) -> Vector2 { + // TODO: Store this X matrix directly in Self? + let X: Matrix2x4 = Matrix2x4::from_fn(|i, j| self.vertices[j][i]); + let N = self.evaluate_basis(xi); + &X * &N.transpose() + } + + #[allow(non_snake_case)] + fn reference_jacobian(&self, xi: &Vector2) -> Matrix2 { + // TODO: Avoid redundant computation of gradient matrix by + // offering a function which simultaneously computes the gradient matrix and the + // Jacobian + let X: Matrix2x4 = Matrix2x4::from_fn(|i, j| self.vertices[j][i]); + let G = self.gradients(xi); + X * G.transpose() + } + + // TODO: Write tests for diameter + fn diameter(&self) -> T { + self.vertices + .iter() + .tuple_combinations() + .map(|(x, y)| distance(x, y)) + .fold(T::zero(), |a, b| a.max(b.clone())) + } +} + +/// A finite element representing linear basis functions on a triangle, in two dimensions. +/// +/// The reference element is chosen to be the triangle defined by the corners +/// (-1, -1), (1, -1), (-1, 1). This perhaps unorthodox choice is due to the quadrature rules +/// we employ. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Tri3d2Element +where + T: Scalar, +{ + vertices: [Point2; 3], +} + +impl Tri3d2Element +where + T: Scalar, +{ + pub fn from_vertices(vertices: [Point2; 3]) -> Self { + Self { vertices } + } + + pub fn vertices(&self) -> &[Point2; 3] { + &self.vertices + } +} + +impl From> for Tri3d2Element +where + T: Scalar, +{ + fn from(triangle: Triangle2d) -> Self { + Self::from_vertices(triangle.0) + } +} + +impl Tri3d2Element +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).unwrap())] + pub fn reference() -> Self { + Self::from_vertices([Point2::new(-1.0, -1.0), Point2::new(1.0, -1.0), Point2::new(-1.0, 1.0)]) + } +} + +impl ElementConnectivity for Tri3d2Connectivity +where + T: RealField, +{ + type Element = Tri3d2Element; + type NodalDim = U3; + type ReferenceDim = U2; + type GeometryDim = U2; + + fn element(&self, vertices: &[Point2]) -> Option { + let Self(indices) = self; + let lookup_vertex = |local_index| vertices.get(indices[local_index]).cloned(); + + Some(Tri3d2Element::from_vertices([ + lookup_vertex(0)?, + lookup_vertex(1)?, + lookup_vertex(2)?, + ])) + } +} + +impl ReferenceFiniteElement for Tri3d2Element +where + T: RealField, +{ + type NodalDim = U3; + type ReferenceDim = U2; + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn evaluate_basis(&self, xi: &Vector2) -> Matrix1x3 { + Matrix1x3::from_row_slice(&[ + -0.5 * xi.x - 0.5 * xi.y, + 0.5 * xi.x + 0.5, + 0.5 * xi.y + 0.5 + ]) + } + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn gradients(&self, _: &Vector2) -> Matrix2x3 { + // TODO: Precompute gradients + Matrix2x3::from_columns(&[ + Vector2::new(-0.5, -0.5), + Vector2::new(0.5, 0.0), + Vector2::new(0.0, 0.5) + ]) + } +} + +impl FiniteElement for Tri3d2Element +where + T: RealField, +{ + type GeometryDim = U2; + + #[allow(non_snake_case)] + fn reference_jacobian(&self, xi: &Vector2) -> Matrix2 { + let X: Matrix2x3 = Matrix2x3::from_fn(|i, j| self.vertices[j][i]); + let G = self.gradients(xi); + X * G.transpose() + } + + #[allow(non_snake_case)] + fn map_reference_coords(&self, xi: &Vector2) -> Vector2 { + // TODO: Store this X matrix directly in Self...? + let X: Matrix2x3 = Matrix2x3::from_fn(|i, j| self.vertices[j][i]); + let N = self.evaluate_basis(xi); + &X * &N.transpose() + } + + // TODO: Write tests for diameter + fn diameter(&self) -> T { + self.vertices + .iter() + .tuple_combinations() + .map(|(x, y)| distance(x, y)) + .fold(T::zero(), |a, b| a.max(b.clone())) + } +} + +/// A finite element representing quadratic basis functions on a triangle, in two dimensions. +/// +/// The reference element is chosen to be the triangle defined by the corners +/// (-1, -1), (1, -1), (-1, 1). This perhaps unorthodox choice is due to the quadrature rules +/// we employ. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Tri6d2Element +where + T: Scalar, +{ + vertices: [Point2; 6], + tri3: Tri3d2Element, +} + +impl Tri6d2Element +where + T: Scalar, +{ + pub fn from_vertices(vertices: [Point2; 6]) -> Self { + let v = &vertices; + let tri = [v[0].clone(), v[1].clone(), v[2].clone()]; + Self { + vertices, + tri3: Tri3d2Element::from_vertices(tri), + } + } + + pub fn vertices(&self) -> &[Point2; 6] { + &self.vertices + } +} + +impl<'a, T> From<&'a Tri3d2Element> for Tri6d2Element +where + T: RealField, +{ + // TODO: Test this + fn from(tri3: &'a Tri3d2Element) -> Self { + let midpoint = |a: &Point2<_>, b: &Point2<_>| LineSegment2d::new(a.clone(), b.clone()).midpoint(); + + let tri3_v = &tri3.vertices; + let mut vertices = [Point2::origin(); 6]; + vertices[0..=2].clone_from_slice(tri3_v); + vertices[3] = midpoint(&tri3_v[0], &tri3_v[1]); + vertices[4] = midpoint(&tri3_v[1], &tri3_v[2]); + vertices[5] = midpoint(&tri3_v[2], &tri3_v[0]); + + Self::from_vertices(vertices) + } +} + +impl<'a, T> From> for Tri6d2Element +where + T: RealField, +{ + fn from(tri3: Tri3d2Element) -> Self { + Self::from(&tri3) + } +} + +impl Tri6d2Element +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).unwrap())] + pub fn reference() -> Self { + Self { + vertices: [ + Point2::new(-1.0, -1.0), + Point2::new(1.0, -1.0), + Point2::new(-1.0, 1.0), + Point2::new(0.0, -1.0), + Point2::new(0.0, 0.0), + Point2::new(-1.0, 0.0), + ], + tri3: Tri3d2Element::reference(), + } + } +} + +impl ElementConnectivity for Tri6d2Connectivity +where + T: RealField, +{ + type Element = Tri6d2Element; + type NodalDim = U6; + type ReferenceDim = U2; + type GeometryDim = U2; + + fn element(&self, vertices: &[Point2]) -> Option { + let Self(indices) = self; + let lookup_vertex = |local_index| vertices.get(indices[local_index]).cloned(); + + Some(Tri6d2Element::from_vertices([ + lookup_vertex(0)?, + lookup_vertex(1)?, + lookup_vertex(2)?, + lookup_vertex(3)?, + lookup_vertex(4)?, + lookup_vertex(5)?, + ])) + } +} + +impl ReferenceFiniteElement for Tri6d2Element +where + T: RealField, +{ + type NodalDim = U6; + type ReferenceDim = U2; + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn evaluate_basis(&self, xi: &Vector2) -> Matrix1x6 { + // We express the basis functions of Tri6 as products of + // the Tri3 basis functions. + let psi = self.tri3.evaluate_basis(xi); + Matrix1x6::from_row_slice(&[ + psi[0] * (2.0 * psi[0] - 1.0), + psi[1] * (2.0 * psi[1] - 1.0), + psi[2] * (2.0 * psi[2] - 1.0), + 4.0 * psi[0] * psi[1], + 4.0 * psi[1] * psi[2], + 4.0 * psi[0] * psi[2], + ]) + } + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn gradients(&self, xi: &Vector2) -> Matrix2x6 { + // Similarly to `evaluate_basis`, we may implement the gradients of + // Tri6 with the help of the function values and gradients of Tri3 + let psi = self.tri3.evaluate_basis(xi); + let g = self.tri3.gradients(xi); + + // Gradient of vertex node i + let vertex_gradient = |i| g.index((.., i)) * (4.0 * psi[i] - 1.0); + + // Gradient of edge node on the edge between vertex i and j + let edge_gradient = |i, j| + g.index((.., i)) * (4.0 * psi[j]) + g.index((.., j)) * (4.0 * psi[i]); + + Matrix2x6::from_columns(&[ + vertex_gradient(0), + vertex_gradient(1), + vertex_gradient(2), + edge_gradient(0, 1), + edge_gradient(1, 2), + edge_gradient(0, 2) + ]) + } +} + +impl FiniteElement for Tri6d2Element +where + T: RealField, +{ + type GeometryDim = U2; + + fn reference_jacobian(&self, xi: &Vector2) -> Matrix2 { + self.tri3.reference_jacobian(xi) + } + + fn map_reference_coords(&self, xi: &Vector2) -> Vector2 { + self.tri3.map_reference_coords(xi) + } + + fn diameter(&self) -> T { + self.tri3.diameter() + } +} + +/// A finite element representing quadratic basis functions on a quad, in two dimensions. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Quad9d2Element +where + T: Scalar, +{ + vertices: [Point2; 9], + // Store quad for easy computation of Jacobians and mapping reference coordinates + quad: Quad4d2Element, +} + +impl Quad9d2Element +where + T: Scalar, +{ + fn from_vertices(vertices: [Point2; 9]) -> Self { + let v = &vertices; + let quad = [v[0].clone(), v[1].clone(), v[2].clone(), v[3].clone()]; + Self { + vertices, + quad: Quad4d2Element::from_vertices(quad), + } + } + + pub fn vertices(&self) -> &[Point2; 9] { + &self.vertices + } +} + +impl<'a, T> From<&'a Quad4d2Element> for Quad9d2Element +where + T: RealField, +{ + fn from(quad4: &'a Quad4d2Element) -> Self { + let midpoint = |a: &Point2<_>, b: &Point2<_>| LineSegment2d::new(a.clone(), b.clone()).midpoint(); + + let quad4_v = &quad4.vertices; + let mut vertices = [Point2::origin(); 9]; + vertices[0..=3].clone_from_slice(quad4_v); + vertices[4] = midpoint(&quad4_v[0], &quad4_v[1]); + vertices[5] = midpoint(&quad4_v[1], &quad4_v[2]); + vertices[6] = midpoint(&quad4_v[2], &quad4_v[3]); + vertices[7] = midpoint(&quad4_v[3], &quad4_v[0]); + + // Vertex 8 is in the middle of the element, i.e. the midpoint + // between 5 and 7 or 4 and 6 (arbitrary choice) + vertices[8] = midpoint(&vertices[4], &vertices[6]); + + Self::from_vertices(vertices) + } +} + +impl<'a, T> From> for Quad9d2Element +where + T: RealField, +{ + fn from(quad4: Quad4d2Element) -> Self { + Self::from(&quad4) + } +} + +impl Quad9d2Element +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + pub fn reference() -> Self { + let p = |x, y| Point2::new(x, y); + Self::from_vertices([ + p(-1.0, -1.0), + p(1.0, -1.0), + p(1.0, 1.0), + p(-1.0, 1.0), + p(0.0, -1.0), + p(1.0, 0.0), + p(0.0, 1.0), + p(-1.0, 0.0), + p(0.0, 0.0), + ]) + } +} + +#[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] +fn quad9_phi_1d(alpha: T, xi: T) -> T +where + T: RealField, +{ + let alpha2 = alpha * alpha; + let a = (3.0 / 2.0) * alpha2 - 1.0; + let b = alpha / 2.0; + let c = 1.0 - alpha2; + a * xi * xi + b * xi + c +} + +impl ReferenceFiniteElement for Quad9d2Element +where + T: RealField, +{ + type ReferenceDim = U2; + type NodalDim = U9; + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn evaluate_basis(&self, xi: &Vector2) -> MatrixMN { + // We define the shape functions as N_{alpha, beta} evaluated at xi such that + // N_{alpha, beta}([alpha, beta]) = 1 + // with alpha, beta = 1 or -1. + // Furthermore, the basis functions are separable in the sense that we may write + // N_{alpha, beta) (xi, eta) = N_alpha(xi) * N_beta(eta). + + let phi_1d = quad9_phi_1d; + let phi = |alpha, beta, xi: &Vector2| { + let x = xi[0]; + let y = xi[1]; + phi_1d(alpha, x) * phi_1d(beta, y) + }; + + MatrixMN::::from_row_slice(&[ + phi(-1.0, -1.0, xi), + phi( 1.0, -1.0, xi), + phi( 1.0, 1.0, xi), + phi(-1.0, 1.0, xi), + phi( 0.0, -1.0, xi), + phi( 1.0, 0.0, xi), + phi( 0.0, 1.0, xi), + phi(-1.0, 0.0, xi), + phi( 0.0, 0.0, xi) + ]) + } + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn gradients(&self, xi: &Vector2) -> MatrixMN { + // See the implementation of `evaluate_basis` for a definition of the basis functions. + let phi_1d = quad9_phi_1d::; + let phi_grad_1d = |alpha, xi| { + let alpha2 = alpha * alpha; + let a = (3.0 / 2.0) * alpha2 - 1.0; + let b = alpha / 2.0; + 2.0 * a * xi + b + }; + + let phi_grad = |alpha, beta, xi: &Vector2| { + let x = xi[0]; + let y = xi[1]; + Vector2::new( + phi_1d(beta, y) * phi_grad_1d(alpha, x), + phi_1d(alpha, x) * phi_grad_1d(beta, y) + ) + }; + + MatrixMN::::from_columns(&[ + phi_grad(-1.0, -1.0, xi), + phi_grad( 1.0, -1.0, xi), + phi_grad( 1.0, 1.0, xi), + phi_grad(-1.0, 1.0, xi), + phi_grad( 0.0, -1.0, xi), + phi_grad( 1.0, 0.0, xi), + phi_grad( 0.0, 1.0, xi), + phi_grad(-1.0, 0.0, xi), + phi_grad( 0.0, 0.0, xi) + ]) + } +} + +impl FiniteElement for Quad9d2Element +where + T: RealField, +{ + type GeometryDim = U2; + + #[allow(non_snake_case)] + fn reference_jacobian(&self, xi: &Vector2) -> Matrix2 { + self.quad.reference_jacobian(xi) + } + + #[allow(non_snake_case)] + fn map_reference_coords(&self, xi: &Vector2) -> Vector2 { + self.quad.map_reference_coords(xi) + } + + // TODO: Write tests for diameter + fn diameter(&self) -> T { + self.quad.diameter() + } +} + +impl ElementConnectivity for Quad9d2Connectivity +where + T: RealField, +{ + type Element = Quad9d2Element; + type NodalDim = U9; + type ReferenceDim = U2; + type GeometryDim = U2; + + fn element(&self, vertices: &[Point2]) -> Option { + let Self(indices) = self; + let mut vertices_array: [Point2; 9] = [Point2::origin(); 9]; + + for (v, global_index) in vertices_array.iter_mut().zip(indices) { + *v = vertices[*global_index]; + } + + Some(Quad9d2Element::from_vertices(vertices_array)) + } +} + +impl TryFrom> for ConvexPolygon +where + T: RealField, +{ + type Error = ConcavePolygonError; + + fn try_from(value: Quad9d2Element) -> Result { + ConvexPolygon::try_from(value.quad) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// A surface element embedded in two dimensions. +pub struct Segment2d2Element +where + T: Scalar, +{ + segment: LineSegment2d, +} + +impl From> for Segment2d2Element +where + T: Scalar, +{ + fn from(segment: LineSegment2d) -> Self { + Self { segment } + } +} + +impl ReferenceFiniteElement for Segment2d2Element +where + T: RealField, +{ + type NodalDim = U2; + type ReferenceDim = U1; + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn evaluate_basis(&self, xi: &Vector1) -> MatrixMN { + // xi is a scalar + let xi = xi.x; + let phi_1 = (1.0 - xi) / 2.0; + let phi_2 = (1.0 + xi) / 2.0; + MatrixMN::<_, U1, U2>::new(phi_1, phi_2) + } + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn gradients(&self, _xi: &Vector1) -> MatrixMN { + MatrixMN::<_, U1, U2>::new(-0.5, 0.5) + } +} + +impl FiniteElement for Segment2d2Element +where + T: RealField, +{ + type GeometryDim = U2; + + #[allow(non_snake_case)] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn reference_jacobian(&self, _xi: &Vector1) -> Vector2 { + let a = &self.segment.from().coords; + let b = &self.segment.to().coords; + (b - a) / 2.0 + } + + #[allow(non_snake_case)] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn map_reference_coords(&self, xi: &Vector1) -> Vector2 { + let a = &self.segment.from().coords; + let b = &self.segment.to().coords; + let phi = self.evaluate_basis(xi); + a * phi[0] + b * phi[1] + } + + fn diameter(&self) -> T { + self.segment.length() + } +} + +impl SurfaceFiniteElement for Segment2d2Element +where + T: RealField, +{ + fn normal(&self, _xi: &Vector1) -> Vector2 { + self.segment.normal_dir().normalize() + } +} + +impl ElementConnectivity for Segment2d2Connectivity +where + T: RealField, +{ + type Element = Segment2d2Element; + type NodalDim = U2; + type ReferenceDim = U1; + type GeometryDim = U2; + + fn element(&self, vertices: &[Point2]) -> Option { + let a = vertices[self.0[0]].clone(); + let b = vertices[self.0[1]].clone(); + let segment = LineSegment2d::new(a, b); + Some(Segment2d2Element::from(segment)) + } +} + +impl ElementConnectivity for Tet4Connectivity +where + T: RealField, +{ + type Element = Tet4Element; + type GeometryDim = U3; + type ReferenceDim = U3; + type NodalDim = U4; + + fn element(&self, vertices: &[Point]) -> Option { + Some(Tet4Element { + vertices: [ + vertices.get(self.0[0])?.clone(), + vertices.get(self.0[1])?.clone(), + vertices.get(self.0[2])?.clone(), + vertices.get(self.0[3])?.clone(), + ], + }) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Tet4Element +where + T: Scalar, +{ + vertices: [Point3; 4], +} + +impl Tet4Element +where + T: Scalar, +{ + pub fn from_vertices(vertices: [Point3; 4]) -> Self { + Self { vertices } + } + + pub fn vertices(&self) -> &[Point3; 4] { + &self.vertices + } +} + +impl Tet4Element +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).unwrap())] + pub fn reference() -> Self { + Self { + vertices: [ + Point3::new(-1.0, -1.0, -1.0), + Point3::new(1.0, -1.0, -1.0), + Point3::new(-1.0, 1.0, -1.0), + Point3::new(-1.0, -1.0, 1.0), + ], + } + } +} + +#[replace_float_literals(T::from_f64(literal).unwrap())] +impl ReferenceFiniteElement for Tet4Element +where + T: RealField, +{ + type ReferenceDim = U3; + type NodalDim = U4; + + #[rustfmt::skip] + fn evaluate_basis(&self, xi: &Vector3) -> Matrix1x4 { + Matrix1x4::from_row_slice(&[ + -0.5 * xi.x - 0.5 * xi.y - 0.5 * xi.z - 0.5, + 0.5 * xi.x + 0.5, + 0.5 * xi.y + 0.5, + 0.5 * xi.z + 0.5 + ]) + } + + #[rustfmt::skip] + fn gradients(&self, _reference_coords: &Vector3) -> Matrix3x4 { + Matrix3x4::from_columns(&[ + Vector3::new(-0.5, -0.5, -0.5), + Vector3::new(0.5, 0.0, 0.0), + Vector3::new(0.0, 0.5, 0.0), + Vector3::new(0.0, 0.0, 0.5) + ]) + } +} + +impl FiniteElement for Tet4Element +where + T: RealField, +{ + type GeometryDim = U3; + + #[allow(non_snake_case)] + fn reference_jacobian(&self, xi: &Vector3) -> Matrix3 { + // TODO: Could store this matrix directly in the element, in order + // to avoid repeated computation + let X = Matrix3x4::from_fn(|i, j| self.vertices[j][i]); + let G = self.gradients(xi); + X * G.transpose() + } + + #[allow(non_snake_case)] + fn map_reference_coords(&self, xi: &Vector3) -> Vector3 { + // TODO: Store this X matrix directly in Self...? + let X = Matrix3x4::from_fn(|i, j| self.vertices[j][i]); + let N = self.evaluate_basis(xi); + &X * &N.transpose() + } + + // TODO: Write tests for diameter + fn diameter(&self) -> T { + self.vertices + .iter() + .tuple_combinations() + .map(|(x, y)| distance(x, y)) + .fold(T::zero(), |a, b| a.max(b.clone())) + } +} + +impl ElementConnectivity for Hex8Connectivity +where + T: RealField, +{ + type Element = Hex8Element; + type GeometryDim = U3; + type ReferenceDim = U3; + type NodalDim = U8; + + fn element(&self, vertices: &[Point]) -> Option { + Some(Hex8Element::from_vertices([ + vertices.get(self.0[0])?.clone(), + vertices.get(self.0[1])?.clone(), + vertices.get(self.0[2])?.clone(), + vertices.get(self.0[3])?.clone(), + vertices.get(self.0[4])?.clone(), + vertices.get(self.0[5])?.clone(), + vertices.get(self.0[6])?.clone(), + vertices.get(self.0[7])?.clone(), + ])) + } +} + +/// Linear basis function on the interval [-1, 1]. +/// +///`alpha == -1` denotes the basis function associated with the node at `x == -1`, +/// and `alpha == 1` for `x == 1`. +#[replace_float_literals(T::from_f64(literal).unwrap())] +#[inline(always)] +fn phi_linear_1d(alpha: T, xi: T) -> T +where + T: RealField, +{ + (1.0 + alpha * xi) / 2.0 +} + +/// Gradient for the linear basis function on the interval [-1, 1]. +/// +/// See `phi_linear_1d` for the meaning of `alpha`. +#[replace_float_literals(T::from_f64(literal).unwrap())] +#[inline(always)] +fn phi_linear_1d_grad(alpha: T) -> T +where + T: RealField, +{ + alpha / 2.0 +} + +/// Quadratic basis function on the interval [-1, 1]. +/// +/// `alpha == -1` denotes the basis function associated with the node at `x == -1`, +/// `alpha == 0` denotes the basis function associated with the node at `x == 0`, +/// and `alpha == 1` for `x == 1`. +#[replace_float_literals(T::from_f64(literal).unwrap())] +#[inline(always)] +fn phi_quadratic_1d(alpha: T, xi: T) -> T +where + T: RealField, +{ + // The compiler should hopefully be able to use constant propagation to + // precompute all expressions involving constants and alpha + let alpha2 = alpha * alpha; + let xi2 = xi * xi; + (3.0 / 2.0 * alpha2 - 1.0) * xi2 + 0.5 * alpha * xi + 1.0 - alpha2 +} + +/// Derivative of quadratic basis function on the interval [-1, 1]. +/// +/// `alpha == -1` denotes the basis function associated with the node at `x == -1`, +/// `alpha == 0` denotes the basis function associated with the node at `x == 0`, +/// and `alpha == 1` for `x == 1`. +#[replace_float_literals(T::from_f64(literal).unwrap())] +#[inline(always)] +fn phi_quadratic_1d_grad(alpha: T, xi: T) -> T +where + T: RealField, +{ + // The compiler should hopefully be able to use constant propagation to + // precompute all expressions involving constants and alpha + let alpha2 = alpha * alpha; + 2.0 * (3.0 / 2.0 * alpha2 - 1.0) * xi + 0.5 * alpha +} + +impl ReferenceFiniteElement for Hex8Element +where + T: RealField, +{ + type ReferenceDim = U3; + type NodalDim = U8; + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn evaluate_basis(&self, xi: &Vector3) -> MatrixMN { + // We define the shape functions as N_{alpha, beta, gamma} evaluated at xi such that + // N_{alpha, beta, gamma}([alpha, beta, gamma]) = 1, + let phi_1d = phi_linear_1d; + let phi = |alpha, beta, gamma, xi: &Vector3| + phi_1d(alpha, xi[0]) * phi_1d(beta, xi[1]) * phi_1d(gamma, xi[2]); + MatrixMN::<_, U1, U8>::from_row_slice(&[ + phi(-1.0, -1.0, -1.0, xi), + phi( 1.0, -1.0, -1.0, xi), + phi( 1.0, 1.0, -1.0, xi), + phi(-1.0, 1.0, -1.0, xi), + phi(-1.0, -1.0, 1.0, xi), + phi( 1.0, -1.0, 1.0, xi), + phi( 1.0, 1.0, 1.0, xi), + phi(-1.0, 1.0, 1.0, xi), + ]) + } + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn gradients(&self, xi: &Vector3) -> MatrixMN { + let phi_1d = phi_linear_1d; + let grad_1d = phi_linear_1d_grad; + let phi_grad = |alpha, beta, gamma, xi: &Vector3| + Vector3::new( + grad_1d(alpha) * phi_1d(beta, xi[1]) * phi_1d(gamma, xi[2]), + phi_1d(alpha, xi[0]) * grad_1d(beta) * phi_1d(gamma, xi[2]), + phi_1d(alpha, xi[0]) * phi_1d(beta, xi[1]) * grad_1d(gamma) + ); + + MatrixMN::from_columns(&[ + phi_grad(-1.0, -1.0, -1.0, xi), + phi_grad( 1.0, -1.0, -1.0, xi), + phi_grad( 1.0, 1.0, -1.0, xi), + phi_grad(-1.0, 1.0, -1.0, xi), + phi_grad(-1.0, -1.0, 1.0, xi), + phi_grad( 1.0, -1.0, 1.0, xi), + phi_grad( 1.0, 1.0, 1.0, xi), + phi_grad(-1.0, 1.0, 1.0, xi), + ]) + } +} + +impl FiniteElement for Hex8Element +where + T: RealField, +{ + type GeometryDim = U3; + + #[allow(non_snake_case)] + fn map_reference_coords(&self, xi: &Vector3) -> Vector3 { + // TODO: Store this X matrix directly in Self...? + let X = MatrixMN::<_, U3, U8>::from_fn(|i, j| self.vertices[j][i]); + let N = self.evaluate_basis(xi); + &X * &N.transpose() + } + + #[allow(non_snake_case)] + fn reference_jacobian(&self, xi: &Vector3) -> Matrix3 { + // TODO: Could store this matrix directly in the element, in order + // to avoid repeated computation + let X = MatrixMN::<_, U3, U8>::from_fn(|i, j| self.vertices[j][i]); + let G = self.gradients(xi); + X * G.transpose() + } + + // TODO: Write tests for diameter + fn diameter(&self) -> T { + self.vertices + .iter() + .tuple_combinations() + .map(|(x, y)| distance(x, y)) + .fold(T::zero(), |a, b| a.max(b.clone())) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Hex8Element { + vertices: [Point3; 8], +} + +impl Hex8Element +where + T: Scalar, +{ + pub fn from_vertices(vertices: [Point3; 8]) -> Self { + Self { vertices } + } + + pub fn vertices(&self) -> &[Point3; 8] { + &self.vertices + } +} + +impl Hex8Element +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + pub fn reference() -> Self { + Self::from_vertices([ + Point3::new(-1.0, -1.0, -1.0), + Point3::new(1.0, -1.0, -1.0), + Point3::new(1.0, 1.0, -1.0), + Point3::new(-1.0, 1.0, -1.0), + Point3::new(-1.0, -1.0, 1.0), + Point3::new(1.0, -1.0, 1.0), + Point3::new(1.0, 1.0, 1.0), + Point3::new(-1.0, 1.0, 1.0), + ]) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Hex27Element { + // Store a hex8 element for trilinear transformations from reference element + hex8: Hex8Element, + vertices: [Point3; 27], +} + +impl Hex27Element { + pub fn from_vertices(vertices: [Point3; 27]) -> Self { + Self { + hex8: Hex8Element::from_vertices(vertices[0..8].try_into().unwrap()), + vertices, + } + } + + pub fn vertices(&self) -> &[Point3] { + &self.vertices + } +} + +impl Hex27Element { + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + pub fn reference() -> Self { + Self::from_vertices([ + Point3::new(-1.0, -1.0, -1.0), + Point3::new(1.0, -1.0, -1.0), + Point3::new(1.0, 1.0, -1.0), + Point3::new(-1.0, 1.0, -1.0), + Point3::new(-1.0, -1.0, 1.0), + Point3::new(1.0, -1.0, 1.0), + Point3::new(1.0, 1.0, 1.0), + Point3::new(-1.0, 1.0, 1.0), + // Edge nodes + Point3::new(0.0, -1.0, -1.0), + Point3::new(-1.0, 0.0, -1.0), + Point3::new(-1.0, -1.0, 0.0), + Point3::new(1.0, 0.0, -1.0), + Point3::new(1.0, -1.0, 0.0), + Point3::new(0.0, 1.0, -1.0), + Point3::new(1.0, 1.0, 0.0), + Point3::new(-1.0, 1.0, 0.0), + Point3::new(0.0, -1.0, 1.0), + Point3::new(-1.0, 0.0, 1.0), + Point3::new(1.0, 0.0, 1.0), + Point3::new(0.0, 1.0, 1.0), + // Face nodes + Point3::new(0.0, 0.0, -1.0), + Point3::new(0.0, -1.0, 0.0), + Point3::new(-1.0, 0.0, 0.0), + Point3::new(1.0, 0.0, 0.0), + Point3::new(0.0, 1.0, 0.0), + Point3::new(0.0, 0.0, 1.0), + // Center node + Point3::new(0.0, 0.0, 0.0), + ]) + } +} + +impl ReferenceFiniteElement for Hex27Element +where + T: RealField, +{ + type ReferenceDim = U3; + type NodalDim = U27; + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn evaluate_basis(&self, xi: &Vector3) -> MatrixMN { + // We define the shape functions as N_{alpha, beta, gamma} evaluated at xi such that + // N_{alpha, beta, gamma}([alpha, beta, gamma]) = 1, + let phi_1d = phi_quadratic_1d; + let phi = |alpha, beta, gamma, xi: &Vector3| + phi_1d(alpha, xi[0]) * phi_1d(beta, xi[1]) * phi_1d(gamma, xi[2]); + MatrixMN::<_, U1, U27>::from_row_slice(&[ + // Vertex nodes + phi(-1.0, -1.0, -1.0, xi), + phi( 1.0, -1.0, -1.0, xi), + phi( 1.0, 1.0, -1.0, xi), + phi(-1.0, 1.0, -1.0, xi), + phi(-1.0, -1.0, 1.0, xi), + phi( 1.0, -1.0, 1.0, xi), + phi( 1.0, 1.0, 1.0, xi), + phi(-1.0, 1.0, 1.0, xi), + + // Edge nodes + phi(0.0, -1.0, -1.0, xi), + phi(-1.0, 0.0, -1.0, xi), + phi(-1.0, -1.0, 0.0, xi), + phi(1.0, 0.0, -1.0, xi), + phi(1.0, -1.0, 0.0, xi), + phi(0.0, 1.0, -1.0, xi), + phi(1.0, 1.0, 0.0, xi), + phi(-1.0, 1.0, 0.0, xi), + phi(0.0, -1.0, 1.0, xi), + phi(-1.0, 0.0, 1.0, xi), + phi(1.0, 0.0, 1.0, xi), + phi(0.0, 1.0, 1.0, xi), + + // Face nodes + phi(0.0, 0.0, -1.0, xi), + phi(0.0, -1.0, 0.0, xi), + phi(-1.0, 0.0, 0.0, xi), + phi(1.0, 0.0, 0.0, xi), + phi(0.0, 1.0, 0.0, xi), + phi(0.0, 0.0, 1.0, xi), + + // Center node + phi(0.0, 0.0, 0.0, xi) + ]) + } + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn gradients(&self, xi: &Vector3) -> MatrixMN { + let phi_1d = phi_quadratic_1d; + let grad_1d = phi_quadratic_1d_grad; + let phi_grad = |alpha, beta, gamma, xi: &Vector3| + Vector3::new( + grad_1d(alpha, xi[0]) * phi_1d(beta, xi[1]) * phi_1d(gamma, xi[2]), + phi_1d(alpha, xi[0]) * grad_1d(beta, xi[1]) * phi_1d(gamma, xi[2]), + phi_1d(alpha, xi[0]) * phi_1d(beta, xi[1]) * grad_1d(gamma, xi[2]) + ); + + MatrixMN::from_columns(&[ + // Vertex nodes + phi_grad(-1.0, -1.0, -1.0, xi), + phi_grad( 1.0, -1.0, -1.0, xi), + phi_grad( 1.0, 1.0, -1.0, xi), + phi_grad(-1.0, 1.0, -1.0, xi), + phi_grad(-1.0, -1.0, 1.0, xi), + phi_grad( 1.0, -1.0, 1.0, xi), + phi_grad( 1.0, 1.0, 1.0, xi), + phi_grad(-1.0, 1.0, 1.0, xi), + + // Edge nodes + phi_grad(0.0, -1.0, -1.0, xi), + phi_grad(-1.0, 0.0, -1.0, xi), + phi_grad(-1.0, -1.0, 0.0, xi), + phi_grad(1.0, 0.0, -1.0, xi), + phi_grad(1.0, -1.0, 0.0, xi), + phi_grad(0.0, 1.0, -1.0, xi), + phi_grad(1.0, 1.0, 0.0, xi), + phi_grad(-1.0, 1.0, 0.0, xi), + phi_grad(0.0, -1.0, 1.0, xi), + phi_grad(-1.0, 0.0, 1.0, xi), + phi_grad(1.0, 0.0, 1.0, xi), + phi_grad(0.0, 1.0, 1.0, xi), + + // Face nodes + phi_grad(0.0, 0.0, -1.0, xi), + phi_grad(0.0, -1.0, 0.0, xi), + phi_grad(-1.0, 0.0, 0.0, xi), + phi_grad(1.0, 0.0, 0.0, xi), + phi_grad(0.0, 1.0, 0.0, xi), + phi_grad(0.0, 0.0, 1.0, xi), + + // Center node + phi_grad(0.0, 0.0, 0.0, xi) + ]) + } +} + +impl FiniteElement for Hex27Element +where + T: RealField, +{ + type GeometryDim = U3; + + fn reference_jacobian(&self, reference_coords: &Vector3) -> Matrix3 { + self.hex8.reference_jacobian(reference_coords) + } + + fn map_reference_coords(&self, reference_coords: &VectorN) -> Vector3 { + self.hex8.map_reference_coords(reference_coords) + } + + fn diameter(&self) -> T { + self.hex8.diameter() + } +} + +impl ElementConnectivity for Hex27Connectivity +where + T: RealField, +{ + type Element = Hex27Element; + type GeometryDim = U3; + type ReferenceDim = U3; + type NodalDim = U27; + + fn element(&self, global_vertices: &[Point3]) -> Option { + let mut hex_vertices = [Point::origin(); 27]; + + for (local_idx, global_idx) in self.0.iter().enumerate() { + hex_vertices[local_idx] = global_vertices.get(*global_idx)?.clone(); + } + + Some(Hex27Element::from_vertices(hex_vertices)) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Hex20Element { + // Store a hex8 element for trilinear transformations from reference element + hex8: Hex8Element, + vertices: [Point3; 20], +} + +impl Hex20Element { + pub fn from_vertices(vertices: [Point3; 20]) -> Self { + Self { + hex8: Hex8Element::from_vertices(vertices[0..8].try_into().unwrap()), + vertices, + } + } + + pub fn vertices(&self) -> &[Point3] { + &self.vertices + } +} + +impl Hex20Element { + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + pub fn reference() -> Self { + Self::from_vertices([ + Point3::new(-1.0, -1.0, -1.0), + Point3::new(1.0, -1.0, -1.0), + Point3::new(1.0, 1.0, -1.0), + Point3::new(-1.0, 1.0, -1.0), + Point3::new(-1.0, -1.0, 1.0), + Point3::new(1.0, -1.0, 1.0), + Point3::new(1.0, 1.0, 1.0), + Point3::new(-1.0, 1.0, 1.0), + // Edge nodes + Point3::new(0.0, -1.0, -1.0), + Point3::new(-1.0, 0.0, -1.0), + Point3::new(-1.0, -1.0, 0.0), + Point3::new(1.0, 0.0, -1.0), + Point3::new(1.0, -1.0, 0.0), + Point3::new(0.0, 1.0, -1.0), + Point3::new(1.0, 1.0, 0.0), + Point3::new(-1.0, 1.0, 0.0), + Point3::new(0.0, -1.0, 1.0), + Point3::new(-1.0, 0.0, 1.0), + Point3::new(1.0, 0.0, 1.0), + Point3::new(0.0, 1.0, 1.0), + ]) + } +} + +impl ReferenceFiniteElement for Hex20Element +where + T: RealField, +{ + type ReferenceDim = U3; + type NodalDim = U20; + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn evaluate_basis(&self, xi: &Vector3) -> MatrixMN { + // We define the shape functions as N_{alpha, beta, gamma} evaluated at xi such that + // N_{alpha, beta, gamma}([alpha, beta, gamma]) = 1, + // but we define corner and edge nodes separately. + + // Formulas are adapted from the following website: + // http://www.softeng.rl.ac.uk/st/projects/felib4/Docs/html/Level-0/brk20/brk20.html + + let phi_corner = |alpha, beta, gamma, xi: &Vector3| + (1.0 / 8.0) * (1.0 + alpha * xi[0]) + * (1.0 + beta * xi[1]) + * (1.0 + gamma * xi[2]) + * (alpha * xi[0] + beta * xi[1] + gamma * xi[2] - 2.0); + + let phi_edge = |alpha, beta, gamma, xi: &Vector3| { + let alpha2 = alpha * alpha; + let beta2 = beta * beta; + let gamma2 = gamma * gamma; + (1.0 / 4.0) * (1.0 - (1.0 - alpha2) * xi[0]*xi[0]) + * (1.0 - (1.0 - beta2) * xi[1]*xi[1]) + * (1.0 - (1.0 - gamma2) * xi[2]*xi[2]) + * (1.0 + alpha * xi[0]) * (1.0 + beta * xi[1]) * (1.0 + gamma * xi[2]) + }; + + MatrixMN::<_, U1, U20>::from_row_slice(&[ + // Corner nodes + phi_corner(-1.0, -1.0, -1.0, xi), + phi_corner( 1.0, -1.0, -1.0, xi), + phi_corner( 1.0, 1.0, -1.0, xi), + phi_corner(-1.0, 1.0, -1.0, xi), + phi_corner(-1.0, -1.0, 1.0, xi), + phi_corner( 1.0, -1.0, 1.0, xi), + phi_corner( 1.0, 1.0, 1.0, xi), + phi_corner(-1.0, 1.0, 1.0, xi), + + // Edge nodes + phi_edge(0.0, -1.0, -1.0, xi), + phi_edge(-1.0, 0.0, -1.0, xi), + phi_edge(-1.0, -1.0, 0.0, xi), + phi_edge(1.0, 0.0, -1.0, xi), + phi_edge(1.0, -1.0, 0.0, xi), + phi_edge(0.0, 1.0, -1.0, xi), + phi_edge(1.0, 1.0, 0.0, xi), + phi_edge(-1.0, 1.0, 0.0, xi), + phi_edge(0.0, -1.0, 1.0, xi), + phi_edge(-1.0, 0.0, 1.0, xi), + phi_edge(1.0, 0.0, 1.0, xi), + phi_edge(0.0, 1.0, 1.0, xi), + ]) + } + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn gradients(&self, xi: &Vector3) -> MatrixMN { + let phi_grad_corner = |alpha, beta, gamma, xi: &Vector3| { + // Decompose shape function as phi(xi) = (1/8) * f(xi) * g(xi), + // with + // f(xi) = sum_i (alpha_i xi_i) - 2 + // g(xi) = product_i (1 + alpha_i xi_i) + // and use product rule to arrive at the below expression + let f = alpha * xi[0] + beta * xi[1] + gamma * xi[2] - 2.0; + let g = (1.0 + alpha * xi[0]) * (1.0 + beta * xi[1]) * (1.0 + gamma * xi[2]); + let s = 1.0 / 8.0; + Vector3::new( + s * (alpha * g + f * alpha * (1.0 + beta * xi[1]) * (1.0 + gamma * xi[2])), + s * (beta * g + f * beta * (1.0 + alpha * xi[0]) * (1.0 + gamma * xi[2])), + s * (gamma * g + f * gamma * (1.0 + alpha * xi[0]) * (1.0 + beta * xi[1])) + ) + }; + + let phi_grad_edge = |alpha, beta, gamma, xi: &Vector3| { + // Decompose shape function as phi(xi) = (1/8) * h(xi) * g(xi), + // with + // h(xi) = product_i (1.0 - (1.0 - alpha_i^2) xi_i^2) + // g(xi) = product_i (1 + alpha_i xi_i) + // and use product rule to arrive at the below expression + let alpha2 = alpha * alpha; + let beta2 = beta * beta; + let gamma2 = gamma * gamma; + let h = (1.0 - (1.0 - alpha2) * xi[0]*xi[0]) + * (1.0 - (1.0 - beta2) * xi[1]*xi[1]) + * (1.0 - (1.0 - gamma2) * xi[2]*xi[2]); + let g = (1.0 + alpha * xi[0]) * (1.0 + beta * xi[1]) * (1.0 + gamma * xi[2]); + let s = 1.0 / 4.0; + + // Note: we hope that the optimizer is able to optimize away most of these operations, + // since alpha2, beta2, gamma2 should be known at compile-time, which + // makes many of the terms here zero. + let dh_xi0 = -2.0 * (1.0 - alpha2) * xi[0] + * (1.0 - (1.0 - beta2) * xi[1]*xi[1]) + * (1.0 - (1.0 - gamma2) * xi[2]*xi[2]); + let dh_xi1 = -2.0 * (1.0 - beta2) * xi[1] + * (1.0 - (1.0 - alpha2) * xi[0] * xi[0]) + * (1.0 - (1.0 - gamma2) * xi[2] * xi[2]); + let dh_xi2 = -2.0 * (1.0 - gamma2) * xi[2] + * (1.0 - (1.0 - alpha2) * xi[0] * xi[0]) + * (1.0 - (1.0 - beta2) * xi[1] * xi[1]); + Vector3::new( + s * (dh_xi0 * g + h * alpha * (1.0 + beta * xi[1]) * (1.0 + gamma * xi[2])), + s * (dh_xi1 * g + h * beta * (1.0 + alpha * xi[0]) * (1.0 + gamma * xi[2])), + s * (dh_xi2 * g + h * gamma * (1.0 + alpha * xi[0]) * (1.0 + beta * xi[1])) + ) + }; + + MatrixMN::from_columns(&[ + // Corner nodes + phi_grad_corner(-1.0, -1.0, -1.0, xi), + phi_grad_corner( 1.0, -1.0, -1.0, xi), + phi_grad_corner( 1.0, 1.0, -1.0, xi), + phi_grad_corner(-1.0, 1.0, -1.0, xi), + phi_grad_corner(-1.0, -1.0, 1.0, xi), + phi_grad_corner( 1.0, -1.0, 1.0, xi), + phi_grad_corner( 1.0, 1.0, 1.0, xi), + phi_grad_corner(-1.0, 1.0, 1.0, xi), + + // Edge nodes + phi_grad_edge(0.0, -1.0, -1.0, xi), + phi_grad_edge(-1.0, 0.0, -1.0, xi), + phi_grad_edge(-1.0, -1.0, 0.0, xi), + phi_grad_edge(1.0, 0.0, -1.0, xi), + phi_grad_edge(1.0, -1.0, 0.0, xi), + phi_grad_edge(0.0, 1.0, -1.0, xi), + phi_grad_edge(1.0, 1.0, 0.0, xi), + phi_grad_edge(-1.0, 1.0, 0.0, xi), + phi_grad_edge(0.0, -1.0, 1.0, xi), + phi_grad_edge(-1.0, 0.0, 1.0, xi), + phi_grad_edge(1.0, 0.0, 1.0, xi), + phi_grad_edge(0.0, 1.0, 1.0, xi), + ]) + } +} + +impl FiniteElement for Hex20Element +where + T: RealField, +{ + type GeometryDim = U3; + + fn reference_jacobian(&self, reference_coords: &Vector3) -> Matrix3 { + self.hex8.reference_jacobian(reference_coords) + } + + fn map_reference_coords(&self, reference_coords: &VectorN) -> Vector3 { + self.hex8.map_reference_coords(reference_coords) + } + + fn diameter(&self) -> T { + self.hex8.diameter() + } +} + +impl ElementConnectivity for Hex20Connectivity +where + T: RealField, +{ + type Element = Hex20Element; + type GeometryDim = U3; + type ReferenceDim = U3; + type NodalDim = U20; + + fn element(&self, global_vertices: &[Point3]) -> Option { + let mut hex_vertices = [Point::origin(); 20]; + + for (local_idx, global_idx) in self.0.iter().enumerate() { + hex_vertices[local_idx] = global_vertices.get(*global_idx)?.clone(); + } + + Some(Hex20Element::from_vertices(hex_vertices)) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// A (surface) finite element representing linear basis functions on a triangle, +/// in three dimensions. +/// +/// The reference element is chosen to be the triangle defined by the corners +/// (-1, -1), (1, -1), (-1, 1). This perhaps unorthodox choice is due to the quadrature rules +/// we employ. +pub struct Tri3d3Element +where + T: Scalar, +{ + triangle: Triangle3d, +} + +impl From> for Tri3d3Element +where + T: Scalar, +{ + fn from(triangle: Triangle3d) -> Self { + Self { triangle } + } +} + +impl ElementConnectivity for Tri3d3Connectivity +where + T: RealField, +{ + type Element = Tri3d3Element; + type NodalDim = U3; + type ReferenceDim = U2; + type GeometryDim = U3; + + fn element(&self, vertices: &[Point3]) -> Option { + let Self(indices) = self; + let lookup_vertex = |local_index| vertices.get(indices[local_index]).cloned(); + + Some(Tri3d3Element::from(Triangle([ + lookup_vertex(0)?, + lookup_vertex(1)?, + lookup_vertex(2)?, + ]))) + } +} + +impl ReferenceFiniteElement for Tri3d3Element +where + T: RealField, +{ + type NodalDim = U3; + type ReferenceDim = U2; + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn evaluate_basis(&self, xi: &Vector2) -> Matrix1x3 { + // TODO: Reuse implementation from Trid2Element instead + Matrix1x3::from_row_slice(&[ + -0.5 * xi[0] - 0.5 * xi[1], + 0.5 * xi[0] + 0.5, + 0.5 * xi[1] + 0.5 + ]) + } + + #[rustfmt::skip] + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn gradients(&self, _: &Vector2) -> Matrix2x3 { + // TODO: Reuse implementation from Trid2Element instead + // TODO: Precompute gradients + Matrix2x3::from_columns(&[ + Vector2::new(-0.5, -0.5), + Vector2::new(0.5, 0.0), + Vector2::new(0.0, 0.5) + ]) + } +} + +impl FiniteElement for Tri3d3Element +where + T: RealField, +{ + type GeometryDim = U3; + + #[allow(non_snake_case)] + fn reference_jacobian(&self, xi: &Vector2) -> Matrix3x2 { + let X: Matrix3 = Matrix3::from_fn(|i, j| self.triangle.0[j][i]); + let G = self.gradients(xi); + X * G.transpose() + } + + #[allow(non_snake_case)] + fn map_reference_coords(&self, xi: &Vector2) -> Vector3 { + // TODO: Store this X matrix directly in Self...? + let X: Matrix3 = Matrix3::from_fn(|i, j| self.triangle.0[j][i]); + let N = self.evaluate_basis(xi); + &X * &N.transpose() + } + + // TODO: Write tests for diameter + fn diameter(&self) -> T { + self.triangle + .0 + .iter() + .tuple_combinations() + .map(|(x, y)| distance(x, y)) + .fold(T::zero(), |a, b| a.max(b.clone())) + } +} + +impl SurfaceFiniteElement for Tri3d3Element +where + T: RealField, +{ + fn normal(&self, _xi: &Vector2) -> Vector3 { + self.triangle.normal() + } +} + +impl ElementConnectivity for Tet10Connectivity +where + T: RealField, +{ + type Element = Tet10Element; + type GeometryDim = U3; + type ReferenceDim = U3; + type NodalDim = U10; + + fn element(&self, vertices: &[Point]) -> Option { + let mut tet10_vertices = [Point3::origin(); 10]; + for (i, v) in tet10_vertices.iter_mut().enumerate() { + *v = vertices.get(self.0[i])?.clone(); + } + + let mut tet4_vertices = [Point3::origin(); 4]; + tet4_vertices.copy_from_slice(&tet10_vertices[0..4]); + + Some(Tet10Element { + tet4: Tet4Element::from_vertices(tet4_vertices), + vertices: tet10_vertices, + }) + } +} + +impl ElementConnectivity for Tet20Connectivity +where + T: RealField, +{ + type Element = Tet20Element; + type GeometryDim = U3; + type ReferenceDim = U3; + type NodalDim = U20; + + fn element(&self, vertices: &[Point]) -> Option { + let mut tet20_vertices = [Point3::origin(); 20]; + for (i, v) in tet20_vertices.iter_mut().enumerate() { + *v = vertices.get(self.0[i])?.clone(); + } + + let mut tet4_vertices = [Point3::origin(); 4]; + tet4_vertices.copy_from_slice(&tet20_vertices[0..4]); + + Some(Tet20Element { + tet4: Tet4Element::from_vertices(tet4_vertices), + vertices: tet20_vertices, + }) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Tet10Element +where + T: Scalar, +{ + tet4: Tet4Element, + vertices: [Point3; 10], +} + +impl Tet10Element +where + T: Scalar, +{ + pub fn from_vertices(vertices: [Point3; 10]) -> Self { + let tet4_v = [ + vertices[0].clone(), + vertices[1].clone(), + vertices[2].clone(), + vertices[3].clone(), + ]; + Self { + tet4: Tet4Element::from_vertices(tet4_v), + vertices, + } + } + + pub fn vertices(&self) -> &[Point3; 10] { + &self.vertices + } +} + +impl Tet10Element +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).unwrap())] + pub fn reference() -> Self { + Self { + tet4: Tet4Element::reference(), + vertices: [ + Point3::new(-1.0, -1.0, -1.0), + Point3::new(1.0, -1.0, -1.0), + Point3::new(-1.0, 1.0, -1.0), + Point3::new(-1.0, -1.0, 1.0), + Point3::new(0.0, -1.0, -1.0), + Point3::new(0.0, 0.0, -1.0), + Point3::new(-1.0, 0.0, -1.0), + Point3::new(-1.0, -1.0, 0.0), + Point3::new(-1.0, 0.0, 0.0), + Point3::new(0.0, -1.0, 0.0), + ], + } + } +} + +#[replace_float_literals(T::from_f64(literal).unwrap())] +impl ReferenceFiniteElement for Tet10Element +where + T: RealField, +{ + type ReferenceDim = U3; + type NodalDim = U10; + + #[rustfmt::skip] + fn evaluate_basis(&self, xi: &Vector3) -> MatrixMN { + // We express the basis functions of Tet10 as products of + // the Tet4 basis functions. + let psi = self.tet4.evaluate_basis(xi); + MatrixMN::from([ + psi[0] * (2.0 * psi[0] - 1.0), + psi[1] * (2.0 * psi[1] - 1.0), + psi[2] * (2.0 * psi[2] - 1.0), + psi[3] * (2.0 * psi[3] - 1.0), + 4.0 * psi[0] * psi[1], + 4.0 * psi[1] * psi[2], + 4.0 * psi[0] * psi[2], + 4.0 * psi[0] * psi[3], + 4.0 * psi[2] * psi[3], + 4.0 * psi[1] * psi[3] + ]) + } + + #[rustfmt::skip] + fn gradients(&self, xi: &Vector3) -> MatrixMN { + // Similarly to `evaluate_basis`, we may implement the gradients of + // Tet10 with the help of the function values and gradients of Tet4 + let psi = self.tet4.evaluate_basis(xi); + let g = self.tet4.gradients(xi); + + // Gradient of vertex node i + let vertex_gradient = |i| g.index((.., i)) * (4.0 * psi[i] - 1.0); + + // Gradient of edge node on the edge between vertex i and j + let edge_gradient = |i, j| + g.index((.., i)) * (4.0 * psi[j]) + g.index((.., j)) * (4.0 * psi[i]); + + MatrixMN::from_columns(&[ + vertex_gradient(0), + vertex_gradient(1), + vertex_gradient(2), + vertex_gradient(3), + edge_gradient(0, 1), + edge_gradient(1, 2), + edge_gradient(0, 2), + edge_gradient(0, 3), + edge_gradient(2, 3), + edge_gradient(1, 3) + ]) + } +} + +impl FiniteElement for Tet10Element +where + T: RealField, +{ + type GeometryDim = U3; + + #[allow(non_snake_case)] + fn reference_jacobian(&self, xi: &Vector3) -> Matrix3 { + self.tet4.reference_jacobian(xi) + } + + #[allow(non_snake_case)] + fn map_reference_coords(&self, xi: &Vector3) -> Vector3 { + self.tet4.map_reference_coords(xi) + } + + // TODO: Write tests for diameter + fn diameter(&self) -> T { + self.tet4.diameter() + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Tet20Element +where + T: Scalar, +{ + tet4: Tet4Element, + vertices: [Point3; 20], +} + +impl Tet20Element +where + T: Scalar, +{ + pub fn from_vertices(vertices: [Point3; 20]) -> Self { + let tet4_v = [ + vertices[0].clone(), + vertices[1].clone(), + vertices[2].clone(), + vertices[3].clone(), + ]; + Self { + tet4: Tet4Element::from_vertices(tet4_v), + vertices, + } + } + + pub fn vertices(&self) -> &[Point3; 20] { + &self.vertices + } +} + +impl Tet20Element +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).unwrap())] + pub fn reference() -> Self { + Self { + tet4: Tet4Element::reference(), + vertices: [ + // Vertex nodes + Point3::new(-1.0, -1.0, -1.0), + Point3::new(1.0, -1.0, -1.0), + Point3::new(-1.0, 1.0, -1.0), + Point3::new(-1.0, -1.0, 1.0), + // Edge nodes + // Between node 0 and 1 + Point3::new(-1.0 / 3.0, -1.0, -1.0), + Point3::new(1.0 / 3.0, -1.0, -1.0), + // Between node 0 and 2 + Point3::new(-1.0, -1.0 / 3.0, -1.0), + Point3::new(-1.0, 1.0 / 3.0, -1.0), + // Between node 0 and 3 + Point3::new(-1.0, -1.0, -1.0 / 3.0), + Point3::new(-1.0, -1.0, 1.0 / 3.0), + // Between node 1 and 2 + Point3::new(1.0 / 3.0, -1.0 / 3.0, -1.0), + Point3::new(-1.0 / 3.0, 1.0 / 3.0, -1.0), + // Between node 1 and 3 + Point3::new(1.0 / 3.0, -1.0, -1.0 / 3.0), + Point3::new(-1.0 / 3.0, -1.0, 1.0 / 3.0), + // Between node 2 and 3 + Point3::new(-1.0, 1.0 / 3.0, -1.0 / 3.0), + Point3::new(-1.0, -1.0 / 3.0, 1.0 / 3.0), + // On face {0, 1, 2} + Point3::new(-1.0 / 3.0, -1.0 / 3.0, -1.0), + // On face {0, 1, 3} + Point3::new(-1.0 / 3.0, -1.0, -1.0 / 3.0), + // On face {0, 2, 3} + Point3::new(-1.0, -1.0 / 3.0, -1.0 / 3.0), + // On face {1, 2, 3} + Point3::new(-1.0 / 3.0, -1.0 / 3.0, -1.0 / 3.0), + ], + } + } +} + +#[replace_float_literals(T::from_f64(literal).unwrap())] +impl ReferenceFiniteElement for Tet20Element +where + T: RealField, +{ + type ReferenceDim = U3; + type NodalDim = U20; + + #[rustfmt::skip] + fn evaluate_basis(&self, xi: &Vector3) -> MatrixMN { + // We express the basis functions of Tet10 as products of + // the Tet4 basis functions. See Zienkiewicz et al., Finite Element Method + // for the basis functions + let psi = self.tet4.evaluate_basis(xi); + + // We define the edge functions by associating a particular edge node + // with its closest vertex. + let phi_edge = |closest: usize, other: usize| + (9.0 / 2.0) * psi[closest] * psi[other] * (3.0 * psi[closest] - 1.0); + // The face functions are associated with the three vertex nodes + // that make up each facec + let phi_face = |a: usize, b: usize, c: usize| + 27.0 * psi[a] * psi[b] * psi[c]; + + MatrixMN::<_, U1, U20>::from_row_slice(&[ + // Corner nodes + 0.5 * psi[0] * (3.0 * psi[0] - 1.0) * (3.0 * psi[0] - 2.0), + 0.5 * psi[1] * (3.0 * psi[1] - 1.0) * (3.0 * psi[1] - 2.0), + 0.5 * psi[2] * (3.0 * psi[2] - 1.0) * (3.0 * psi[2] - 2.0), + 0.5 * psi[3] * (3.0 * psi[3] - 1.0) * (3.0 * psi[3] - 2.0), + + // Edge nodes + // Between node 0 and 1 + phi_edge(0, 1), + phi_edge(1, 0), + // Between node 0 and 2 + phi_edge(0, 2), + phi_edge(2, 0), + // Between node 0 and 3 + phi_edge(0, 3), + phi_edge(3, 0), + // Between node 1 and 2 + phi_edge(1, 2), + phi_edge(2, 1), + // Between node 1 and 3 + phi_edge(1, 3), + phi_edge(3, 1), + // Between node 2 and 3 + phi_edge(2, 3), + phi_edge(3, 2), + + // Faces nodes + // On face {0, 1, 2} + phi_face(0, 1, 2), + // On face {0, 1, 3} + phi_face(0, 1, 3), + // On face {0, 2, 3} + phi_face(0, 2, 3), + // On face {1, 2, 3} + phi_face(1, 2, 3), + ]) + } + + #[rustfmt::skip] + fn gradients(&self, xi: &Vector3) -> MatrixMN { + // Similarly to `evaluate_basis`, we may implement the gradients of + // Tet10 with the help of the function values and gradients of Tet4 + let psi = self.tet4.evaluate_basis(xi); + let tet4_gradients = self.tet4.gradients(xi); + let g = |i| tet4_gradients.index((.., i)); + + // Gradient of vertex node i + let vertex_gradient = |i| -> Vector3 { + let p = psi[i]; + g(i) * 0.5 * (27.0 * p * p - 18.0 * p + 2.0) + }; + + // Gradient of edge node on the edge between vertex a and b + let edge_gradient = |a, b| -> Vector3 { + let pa = psi[a]; + let pb = psi[b]; + ( g(a) * (pb * (6.0 * pa - 1.0)) + g(b) * (pa * (3.0 * pa - 1.0))) * (9.0 / 2.0) + }; + + let face_gradient = |a, b, c| -> Vector3 { + (g(a) * psi[b] * psi[c] + g(b) * psi[a] * psi[c] + g(c) * psi[a] * psi[b]) * 27.0 + }; + + MatrixMN::from_columns(&[ + // Vertex nodes + vertex_gradient(0), + vertex_gradient(1), + vertex_gradient(2), + vertex_gradient(3), + + // Edge nodes + // Between node 0 and 1 + edge_gradient(0, 1), + edge_gradient(1, 0), + // Between node 0 and 2 + edge_gradient(0, 2), + edge_gradient(2, 0), + // Between node 0 and 3 + edge_gradient(0, 3), + edge_gradient(3, 0), + // Between node 1 and 2 + edge_gradient(1, 2), + edge_gradient(2, 1), + // Between node 1 and 3 + edge_gradient(1, 3), + edge_gradient(3, 1), + // Between node 2 and 3 + edge_gradient(2, 3), + edge_gradient(3, 2), + + // Faces nodes + // On face {0, 1, 2} + face_gradient(0, 1, 2), + // On face {0, 1, 3} + face_gradient(0, 1, 3), + // On face {0, 2, 3} + face_gradient(0, 2, 3), + // On face {1, 2, 3} + face_gradient(1, 2, 3), + ]) + } +} + +impl FiniteElement for Tet20Element +where + T: RealField, +{ + type GeometryDim = U3; + + #[allow(non_snake_case)] + fn reference_jacobian(&self, xi: &Vector3) -> Matrix3 { + self.tet4.reference_jacobian(xi) + } + + #[allow(non_snake_case)] + fn map_reference_coords(&self, xi: &Vector3) -> Vector3 { + self.tet4.map_reference_coords(xi) + } + + // TODO: Write tests for diameter + fn diameter(&self) -> T { + self.tet4.diameter() + } +} + +/// Maps physical coordinates `x` to reference coordinates `xi` by solving the equation +/// x - T(xi) = 0 using Newton's method. +/// +pub fn map_physical_coordinates( + element: &Element, + x: &Point, +) -> Result, Box> +where + T: RealField, + Element: FiniteElement, + GeometryDim: DimName + DimMin, + DefaultAllocator: VolumeFiniteElementAllocator, +{ + use hamilton2::calculus::VectorFunctionBuilder; + use hamilton2::newton::newton; + + let f = VectorFunctionBuilder::with_dimension(GeometryDim::dim()) + .with_function(|f, xi| { + // Need to create stack-allocated xi + let xi = xi.fixed_slice::(0, 0).clone_owned(); + f.copy_from(&(element.map_reference_coords(&xi) - &x.coords)); + }) + .with_jacobian_solver( + |sol: &mut DVectorSliceMut, xi: &DVectorSlice, rhs: &DVectorSlice| { + let xi = xi.fixed_slice::(0, 0).clone_owned(); + let j = element.reference_jacobian(&xi); + let lu = j.full_piv_lu(); + sol.copy_from(rhs); + if lu.solve_mut(sol) { + Ok(()) + } else { + Err(Box::::from( + "LU decomposition failed. Jacobian not invertible?", + )) + } + }, + ); + + // We solve the equation T(xi) = x, i.e. we seek reference coords xi such that when + // transformed to physical coordinates yield x. We note here that what Newton's method solves + // is the system T(xi) - x = 0, which can be re-interpreted as finding xi such that + // T_trans(xi) = 0, with T_trans(xi) = T(xi) - x. + // This means that we seek xi such that the translated transformation transforms xi to + // the zero vector. Since x should be a point in the element, it follows that we can expect + // the diameter of the element to give us a representative scale of the "size" of x, + // so we can construct our convergence criterion as follows: + // ||T(x_i) - x|| <= eps * diameter + // with eps some small constant. + + let settings = NewtonSettings { + // Note: Max iterations is entirely random at this point. Should of course + // be made configurable. TODO + max_iterations: Some(20), + // TODO: eps is here hard-coded without respect to the type T, so it will not be appropriate + // across e.g. different floating point types. Fix this! + tolerance: T::from_f64(1e-12).unwrap() * element.diameter(), + }; + + let mut xi = VectorN::::zeros(); + let mut f_val = VectorN::::zeros(); + let mut dx = VectorN::::zeros(); + + // Because we cannot prove to the compiler that the strides of `VectorN` + // are compatible (in a `DimEq` sense) without nasty additional trait bounds, + // we first take slices of the vectors so that the stride is dynamic. At this point, + // it is known that `DimEq` works, so we can use it with `newton`, + // `which expects `Into>`. + macro_rules! slice { + ($e:expr) => { + $e.fixed_slice_with_steps_mut::((0, 0), (0, 0)) + }; + } + + newton(f, &mut slice!(xi), &mut slice!(f_val), &mut slice!(dx), settings)?; + + Ok(Point::from(xi)) +} + +/// Projects physical coordinates `x` to reference coordinates `xi` by solving the equation +/// x - T(xi) = 0 using a generalized form of Newton's method. +/// +/// Unlike `map_physical_coordinates`, this method is also applicable to e.g. surface finite +/// elements, in which the reference dimension and geometry dimension differ. +/// +/// The method panics if `ReferenceDim` is greater than `GeometryDim`. +/// +#[allow(non_snake_case)] +pub fn project_physical_coordinates( + element: &Element, + x: &Point, +) -> Result, Box> +where + T: RealField, + Element: FiniteElement, + Element::ReferenceDim: DimName + DimMin, + DefaultAllocator: FiniteElementAllocator, +{ + assert!( + Element::ReferenceDim::dim() <= Element::GeometryDim::dim(), + "ReferenceDim must be smaller or equal to GeometryDim." + ); + + // See comments in `map_physical_coordinates` for why this is a reasonable tolerance. + let tolerance = T::from_f64(1e-12).unwrap() * element.diameter(); + + // We wish to solve the system + // f(xi) - x = 0, + // but xi and x have different dimensions. To overcome this difficulty, we use a modified + // version of Newton's method in which we solve the normal equations for the Jacobian equation + // instead of solving the Jacobian system directly (which is underdetermined in this case). + // + // Our stopping condition is based on the optimality condition for the + // least-squares problem min || x - f(xi) ||, whose geometrical interpretation at the + // minimum is exactly that of a projection onto the surface. + + let x = &x.coords; + let mut xi = VectorN::::zeros(); + let mut f = element.map_reference_coords(&xi); + let mut j = element.reference_jacobian(&xi); + let mut jT = j.transpose(); + + let mut iter = 0; + // TODO: Do we need to alter the tolerance due to the jT term? + while (&jT * (&f - x)).norm() > tolerance { + let jTj = &jT * j; + let lu = jTj.full_piv_lu(); + let rhs = -jT * (&f - x); + + if let Some(sol) = lu.solve(&rhs) { + xi += sol; + } else { + return Err(Box::from( + "LU decomposition failed. Normal equation for Jacobian not invertible?", + )); + } + + f = element.map_reference_coords(&xi); + j = element.reference_jacobian(&xi); + jT = j.transpose(); + iter += 1; + + // TODO: Should better handle degenerate/problematic cases. For now we just want to + // avoid infinite loops + if iter > 1000 { + eprintln!("Exceeded 1000 iterations for project_physical_coordinates"); + } + } + + Ok(Point::from(xi)) +} diff --git a/fenris/src/embedding/embedding2d.rs b/fenris/src/embedding/embedding2d.rs new file mode 100644 index 0000000..61a3970 --- /dev/null +++ b/fenris/src/embedding/embedding2d.rs @@ -0,0 +1,334 @@ +use crate::connectivity::{ + CellConnectivity, Quad4d2Connectivity, Quad9d2Connectivity, Tri3d2Connectivity, Tri6d2Connectivity, +}; +use crate::element::{map_physical_coordinates, ElementConnectivity, FiniteElement, Tri3d2Element}; +use crate::geometry::ConvexPolygon; +use crate::mesh::Mesh2d; +use crate::quadrature::Quadrature2d; +use itertools::{izip, Either, Itertools}; +use nalgebra::{DefaultAllocator, DimNameMul, Point2, RealField, Vector2, U2}; +use std::collections::HashSet; +use std::convert::TryFrom; +use std::error::Error; +use std::iter::repeat; + +use crate::allocators::VolumeFiniteElementAllocator; +use crate::connectivity::CellFace; +use crate::embedding::{EmbeddedModel2d, EmbeddedModelBuilder, EmbeddedQuadrature}; + +pub type EmbeddedQuad4Model = EmbeddedModel2d; +pub type EmbeddedQuad9Model = EmbeddedModel2d; +pub type EmbeddedTri3Model = EmbeddedModel2d; +pub type EmbeddedTri6Model = EmbeddedModel2d; + +fn try_convert_cells_to_polygons( + vertices: &[Point2], + connectivity: &[Connectivity], +) -> Result>, String> +where + T: RealField, + Connectivity: CellConnectivity, + ConvexPolygon: TryFrom, +{ + let mut polygons = Vec::new(); + + for conn in connectivity { + let cell = conn.cell(vertices).ok_or(String::from( + "Failed to construct cell from vertices. Index out of bounds?", + ))?; + let polygon = + ConvexPolygon::try_from(cell).map_err(|_| String::from("Failed to construct convex polygon from cell."))?; + polygons.push(polygon); + } + + Ok(polygons) +} + +/// Given a background mesh and an embedded mesh, find the indices of background cells +/// that intersect the embedded mesh. +pub fn find_background_cell_indices_2d( + background_mesh: &Mesh2d, + embedded_mesh: &Mesh2d, +) -> Result, String> +where + T: RealField, + BgCell: CellConnectivity, + EmbedCell: CellConnectivity, + ConvexPolygon: TryFrom, + ConvexPolygon: TryFrom, +{ + let embedded_polygons = try_convert_cells_to_polygons(embedded_mesh.vertices(), embedded_mesh.connectivity())?; + let mut indices = Vec::new(); + + for (i, connectivity) in background_mesh.connectivity().iter().enumerate() { + let cell = connectivity + .cell(background_mesh.vertices()) + .ok_or(String::from( + "Failed to construct background cell from vertices. Index out of bounds?", + ))?; + let cell_polygon = ConvexPolygon::try_from(cell) + .map_err(|_| String::from("Failed to create convex polygon from background cell."))?; + + let intersects_embedded = embedded_polygons + .iter() + .any(|embedded_poly| !cell_polygon.intersect_polygon(embedded_poly).is_empty()); + + if intersects_embedded { + indices.push(i); + } + } + + Ok(indices) +} + +/// Given a background mesh and an embedded mesh (a mesh that is embedded into the background mesh), +/// return the indices of the background cells that intersect the boundary interfaces of the +/// embedded mesh. +/// +/// The operation returns an error if any of the cells (background or embedded) are non-convex, +/// or more precisely cannot be convert into a convex polygon. +pub fn find_interface_background_cells_2d( + background_mesh: &Mesh2d, + embedded_mesh: &Mesh2d, +) -> Result, ()> +where + T: RealField, + BgCell: CellConnectivity, + EmbedCell: CellConnectivity, + EmbedCell::FaceConnectivity: CellConnectivity, + ConvexPolygon: TryFrom, + ConvexPolygon: TryFrom>, +{ + let embedded_boundary_faces: Vec<_> = embedded_mesh + .find_boundary_faces() + .into_iter() + .map(|(connectivity, _, _)| { + connectivity + .cell(embedded_mesh.vertices()) + .expect("All embedded mesh vertex indices must be in bounds.") + }) + .map(|face| ConvexPolygon::try_from(face).map_err(|_| ())) + .collect::>()?; + + let mut interface_cells = Vec::new(); + for (i, cell_connectivity) in background_mesh.connectivity().iter().enumerate() { + let cell = cell_connectivity + .cell(background_mesh.vertices()) + .expect("All background mesh vertex indices must be in bounds."); + let cell_poly = ConvexPolygon::try_from(cell).map_err(|_| ())?; + + // Here we use the fact that faces are also polyhedra + // TODO: Use spatial acceleration to improve complexity + let intersects_interface = embedded_boundary_faces + .iter() + .map(|face_poly| cell_poly.intersect_polygon(face_poly)) + .any(|cell_face_intersection| !cell_face_intersection.is_empty()); + + if intersects_interface { + interface_cells.push(i); + } + } + + Ok(interface_cells) +} + +/// Given a background mesh and an embedded mesh (a mesh that is embedded into the background mesh), +/// returns a vector consisting of tuples `(i, poly)` in which `i` is the index of a background cell +/// that intersects the boundary of the embedded mesh, and `poly` is a vector of convex polygons +/// representing the results of intersecting background cell `i` with all embedded cells. +/// +/// The operation returns an error if any of the cells (background or embedded) are non-convex, +/// or more precisely cannot be convert into a convex polygon. +pub fn embed_mesh_2d( + background_mesh: &Mesh2d, + embedded_mesh: &Mesh2d, +) -> Result>)>, ()> +where + T: RealField, + BgCell: CellConnectivity, + EmbedCell: CellConnectivity, + EmbedCell::FaceConnectivity: CellConnectivity, + ConvexPolygon: TryFrom, + ConvexPolygon: TryFrom, + ConvexPolygon: TryFrom>, +{ + let embedded_polygons: Vec<_> = embedded_mesh + .connectivity() + .iter() + .map(|connectivity| { + connectivity + .cell(embedded_mesh.vertices()) + .expect("Embedded cells must not have indices out of bounds") + }) + .map(|cell| ConvexPolygon::try_from(cell).map_err(|_| ())) + .collect::>()?; + + let interface_cell_indices = find_interface_background_cells_2d(background_mesh, embedded_mesh)?; + let mut result = Vec::new(); + + for i in interface_cell_indices { + let connectivity = &background_mesh.connectivity()[i]; + let cell = connectivity + .cell(background_mesh.vertices()) + .expect("Background cells must not have indices out of bounds"); + let cell_poly = ConvexPolygon::try_from(cell).map_err(|_| ())?; + let mut intersections = Vec::new(); + + // TODO: Spatial acceleration + for embedded_poly in &embedded_polygons { + let intersection = cell_poly.intersect_polygon(embedded_poly); + if !intersection.is_empty() { + intersections.push(intersection); + } + } + + result.push((i, intersections)); + } + + Ok(result) +} + +/// Computes intersections between the given polygon and all cells in the embedded geometry. +pub fn embed_cell_2d( + polygon: &ConvexPolygon, + embedded_mesh: &Mesh2d, +) -> Result>, Box> +where + T: RealField, + EmbedCell: CellConnectivity, + ConvexPolygon: TryFrom, +{ + let mut polygon_intersections = Vec::new(); + + for embedded_cell in embedded_mesh.cell_iter() { + let embedded_polygon = ConvexPolygon::try_from(embedded_cell) + .map_err(|_| String::from("Could not convert embedded cell to convex polygon."))?; + + let intersection = polygon.intersect_polygon(&embedded_polygon); + if !intersection.is_empty() { + polygon_intersections.push(intersection); + } + } + + Ok(polygon_intersections) +} + +pub fn construct_embedded_quadrature_for_element_2d( + element: &Element, + intersected_polygons: &[ConvexPolygon], + triangle_quadrature: impl Quadrature2d, +) -> (Vec, Vec>) +where + T: RealField, + Element: FiniteElement, + DefaultAllocator: VolumeFiniteElementAllocator, +{ + intersected_polygons + .iter() + .flat_map(ConvexPolygon::triangulate) + .map(Tri3d2Element::from) + // TODO: Filter out degenerate triangles (they anyway don't contribute to the + // integral value + .flat_map(|tri| izip!(repeat(tri), triangle_quadrature.weights(), triangle_quadrature.points())) + .map(|(tri, w_tri, xi_tri)| { + // Map points and weights in reference element for the triangle to the + // reference element of the background element + let x = tri.map_reference_coords(xi_tri); + let j_tri = tri.reference_jacobian(xi_tri); + let xi_element = map_physical_coordinates(element, &Point2::from(x)).expect("TODO: Handle error"); + let j_element = element.reference_jacobian(&xi_element.coords); + + // Note: we assume that the element is not completely degenerate here + debug_assert!(j_element.determinant() != T::zero()); + let w_element = *w_tri * j_tri.determinant().abs() / j_element.determinant().abs(); + (w_element, xi_element.coords) + }) + .unzip() +} + +/// +/// +/// TODO: Return a proper error type +#[allow(non_snake_case)] +pub fn construct_embedded_quadrature( + embedded_elements: impl IntoIterator>)>, + triangle_quadrature: impl Quadrature2d, +) -> Vec<(Vec, Vec>)> +where + T: RealField, + Element: FiniteElement, + DefaultAllocator: VolumeFiniteElementAllocator, +{ + embedded_elements + .into_iter() + .map(|(element, polygons)| { + construct_embedded_quadrature_for_element_2d(&element, &polygons, &triangle_quadrature) + }) + .collect() +} + +pub fn construct_embedded_model_2d( + background_mesh: &Mesh2d, + embedded_mesh: &Mesh2d, + triangle_quadrature: &impl Quadrature2d, + interior_quadrature: (Vec, Vec>), +) -> Result, Box> +where + T: RealField, + BgConn: CellConnectivity + ElementConnectivity, + EmbedConn: CellConnectivity, + EmbedConn::FaceConnectivity: CellConnectivity, + ConvexPolygon: TryFrom, + ConvexPolygon: TryFrom, + ConvexPolygon: TryFrom>, + DefaultAllocator: VolumeFiniteElementAllocator, + U2: DimNameMul, + BgConn::NodalDim: DimNameMul, +{ + let interface_cell_indices: HashSet = find_interface_background_cells_2d(&background_mesh, embedded_mesh) + .map_err(|_| String::from("Unknown error when finding background interface cells."))? + .into_iter() + .collect(); + + let (interface_connectivity, interior_connectivity): (Vec<_>, Vec<_>) = background_mesh + .connectivity() + .iter() + .cloned() + .enumerate() + .partition_map(|(i, connectivity)| { + if interface_cell_indices.contains(&i) { + Either::Left(connectivity) + } else { + Either::Right(connectivity) + } + }); + + let mut interface_quadratures = Vec::new(); + let mut interface_element_connectivity = Vec::new(); + for connectivity in interface_connectivity { + let cell = connectivity + .cell(background_mesh.vertices()) + .ok_or_else(|| String::from("Failed to construct cell from vertices. Index out of bounds?"))?; + let element = connectivity + .element(background_mesh.vertices()) + .ok_or_else(|| String::from("Failed to construct element from vertices. Index out of bounds?"))?; + let cell_polygon = + ConvexPolygon::try_from(cell).map_err(|_| String::from("Failed to construct convex polygon from cell."))?; + let intersections = embed_cell_2d(&cell_polygon, embedded_mesh)?; + let quadrature = construct_embedded_quadrature_for_element_2d(&element, &intersections, &triangle_quadrature); + interface_quadratures.push(quadrature); + interface_element_connectivity.push(connectivity); + } + + let model = EmbeddedModelBuilder::new() + .vertices(background_mesh.vertices().to_vec()) + .interior_connectivity(interior_connectivity) + .interface_connectivity(interface_element_connectivity) + .catchall_quadrature(EmbeddedQuadrature::from_interior_and_interface( + interior_quadrature, + interface_quadratures, + )) + .build(); + + Ok(model) +} diff --git a/fenris/src/embedding/embedding3d.rs b/fenris/src/embedding/embedding3d.rs new file mode 100644 index 0000000..1996628 --- /dev/null +++ b/fenris/src/embedding/embedding3d.rs @@ -0,0 +1,434 @@ +use crate::allocators::{ElementConnectivityAllocator, FiniteElementAllocator}; +use crate::connectivity::{CellConnectivity, ConnectivityMut}; +use crate::element::{map_physical_coordinates, ElementConnectivity, FiniteElement}; +use crate::geometry::polymesh::PolyMesh3d; +use crate::geometry::{AxisAlignedBoundingBox, BoundedGeometry, ConvexPolyhedron}; +use crate::mesh::{Mesh3d, Tet4Mesh}; +use crate::quadrature::{Quadrature, QuadraturePair3d}; + +use itertools::izip; +use nalgebra::{DefaultAllocator, Point3, RealField, Scalar, U3}; +use numeric_literals::replace_float_literals; +use rayon::prelude::*; +use rstar::RTree; + +use crate::embedding::{EmbeddedModel3d, EmbeddedModelBuilder, EmbeddedQuadrature}; +use std::convert::TryFrom; +use std::error::Error; +use std::iter::repeat; +use std::ops::Add; + +use crate::rtree::{rstar_aabb_from_bounding_box_3d, LabeledAABB3d, LabeledGeometry}; +use nalgebra::allocator::Allocator; + +#[derive(Clone, Debug, PartialEq)] +pub struct Embedding { + /// Background cells that fall outside the embedded geometry. + pub exterior_cells: Vec, + /// Background cells that fall completely inside the embedded geometry. + pub interior_cells: Vec, + /// Background cells that intersect an interface (i.e. boundary) of the + /// embedded geometry. + pub interface_cells: Vec, + /// For each background interface cell, a poly mesh representing the intersection + /// of the embedded geometry and the cell. + pub interface_cell_embeddings: Vec>, +} + +impl Default for Embedding { + fn default() -> Self { + Embedding { + exterior_cells: vec![], + interior_cells: vec![], + interface_cells: vec![], + interface_cell_embeddings: vec![], + } + } +} + +enum CellEmbedding { + Exterior, + Interior, + Interface { intersection: PolyMesh3d }, +} + +#[replace_float_literals(T::from_f64(literal).unwrap())] +fn embed_in_cell<'a, T, Cell>( + cell: &'a Cell, + embedded_mesh: &'a PolyMesh3d, + embedded_cells_rtree: &'a RTree>, + opts: &EmbedOptions, +) -> CellEmbedding +where + T: RealField, + Cell: ConvexPolyhedron<'a, T> + BoundedGeometry, +{ + let aabb = rstar_aabb_from_bounding_box_3d(&cell.bounding_box()); + let mut relevant_embedded_cells: Vec<_> = embedded_cells_rtree + .locate_in_envelope_intersecting(&aabb) + .map(|embedded_cell_candidate| embedded_cell_candidate.label) + .collect(); + + // Sorting here is not technically necessary, but it + // may possibly improve performance if it turns out that the mesh itself + // has a cache-efficient ordering + relevant_embedded_cells.sort_unstable(); + + let embedded_mesh_region = embedded_mesh.keep_cells(&relevant_embedded_cells); + let intersection = embedded_mesh_region.intersect_convex_polyhedron(cell); + + if intersection.num_cells() > 0 { + let bg_cell_volume = cell.compute_volume(); + // Note: Currently, PolyMesh3d::compute_volume() only works if the outside faces are + // correctly oriented, but we haven't properly accounted for this in the + // various mesh processing routines. TODO: We need to fix this long term, + // but for now, we can convert to a tet mesh and compute the volume this way + // (this is not dependent on face orientations to work correctly). + let triangulated_intersection = intersection.triangulate().expect( + "Triangulation should always work in this case, provided that\ + our input mesh is well formed. TODO: This should be verified by\ + PolyMesh constructor.", + ); + let intersected_cell_volume = Tet4Mesh::try_from(&triangulated_intersection) + .expect( + "Conversion to tet mesh cannot fail since we have a valid triangulated\ + PolyMesh.", + ) + .cell_iter() + .map(|cell| cell.compute_volume()) + .fold(T::zero(), Add::add); + + if intersected_cell_volume < opts.lower_volume_threshold * bg_cell_volume { + CellEmbedding::Exterior + } else if intersected_cell_volume < opts.upper_volume_threshold * bg_cell_volume { + CellEmbedding::Interface { intersection } + } else { + CellEmbedding::Interior + } + } else { + CellEmbedding::Exterior + } +} + +/// Options for mesh embedding. +/// +/// ### Thresholds +/// +/// Let `cut_cell_volume` be the volume of the intersection between the embedded mesh and a +/// background cell, and let `bg_cell_volume` be the volume of the background cell. Then: +/// +/// - If `cut_cell_volume < lower_volume_threshold * bg_cell_volume`, then +/// the cell is designated as an "exterior" cell, which is typically removed +/// from the simulation. +/// - If `cut_cell_volume < upper_volume_threshold * bg_cell_volume`, then +/// the cell is designated as an "interface" cell. +/// - Otherwise, it is designated as an "interior" cell. +/// +/// The default settings (`Default::default`) should work well for most/all practical problems. +#[derive(Debug, Clone)] +pub struct EmbedOptions { + pub upper_volume_threshold: T, + pub lower_volume_threshold: T, +} + +impl Default for EmbedOptions { + #[replace_float_literals(T::from_f64(literal).unwrap())] + fn default() -> Self { + Self { + upper_volume_threshold: 0.999, + lower_volume_threshold: 1e-4, + } + } +} + +#[replace_float_literals(T::from_f64(literal).unwrap())] +pub fn embed_mesh_3d( + background_mesh: &Mesh3d, + embedded_mesh: &PolyMesh3d, +) -> Embedding +where + T: RealField, + BgConnectivity: CellConnectivity, + BgConnectivity::Cell: Send + BoundedGeometry + for<'a> ConvexPolyhedron<'a, T>, +{ + embed_mesh_3d_with_opts(background_mesh, embedded_mesh, &EmbedOptions::default()) +} + +#[replace_float_literals(T::from_f64(literal).unwrap())] +pub fn embed_mesh_3d_with_opts( + background_mesh: &Mesh3d, + embedded_mesh: &PolyMesh3d, + opts: &EmbedOptions, +) -> Embedding +where + T: RealField, + BgConnectivity: CellConnectivity, + BgConnectivity::Cell: Send + BoundedGeometry + for<'a> ConvexPolyhedron<'a, T>, +{ + let embedded_cell_bounding_geometries = embedded_mesh + .cell_connectivity_iter() + .enumerate() + .filter_map(|(cell_idx, face_indices)| { + let cell_vertices = face_indices + .iter() + .copied() + .flat_map(|i| embedded_mesh.face_vertices(i)); + AxisAlignedBoundingBox::from_points(cell_vertices) + .map(|bounding_box| LabeledGeometry::new(cell_idx, bounding_box)) + }) + .collect(); + + let embedded_cells_rtree = RTree::bulk_load(embedded_cell_bounding_geometries); + + let background_cells: Vec<_> = background_mesh.cell_iter().collect(); + let cell_embeddings: Vec<_> = background_cells + .into_par_iter() + .map(|cell| embed_in_cell(&cell, embedded_mesh, &embedded_cells_rtree, opts)) + .collect(); + + let mut embedding = Embedding::default(); + for (i, cell_embedding) in cell_embeddings.into_iter().enumerate() { + match cell_embedding { + CellEmbedding::Interface { intersection } => { + embedding.interface_cells.push(i); + embedding.interface_cell_embeddings.push(intersection); + } + CellEmbedding::Interior => { + embedding.interior_cells.push(i); + } + CellEmbedding::Exterior => { + embedding.exterior_cells.push(i); + } + } + } + + embedding +} + +pub struct StabilizationOptions { + // TODO: Only conditionally stabilize? + // Only stabilize if embedded cell volume <= threshold * bg cell volume + // pub relative_volume_threshold: Option, + /// The multiplicative factor for stabilization. + /// + /// The stabilization factor gets multiplied with the quadrature weights of the + /// original quadrature for the background cell. + pub stabilization_factor: T, + /// Quadrature used for stabilization. This should normally correspond to an appropriate-order + /// quadrature rule for the uncut cell. + pub stabilization_quadrature: QuadraturePair3d, +} + +pub struct QuadratureOptions { + pub stabilization: Option>, +} + +impl Default for QuadratureOptions { + fn default() -> Self { + Self { stabilization: None } + } +} + +pub fn compute_element_embedded_quadrature<'a, T, Element>( + bg_element: &'a Element, + embedding: &PolyMesh3d, + tetrahedron_quadrature: impl Quadrature, + quadrature_options: &QuadratureOptions, +) -> Result, Box> +where + T: RealField, + Element: FiniteElement, + DefaultAllocator: FiniteElementAllocator, +{ + let embedded_tet_mesh = Tet4Mesh::try_from(&embedding.triangulate()?)?; + + let (mut w, mut p): QuadraturePair3d<_> = embedded_tet_mesh + .connectivity() + .iter() + .flat_map(|conn| { + // Zip each (repeated) element with the full set of tet quadrature points and weights + // This way we can work on one quadrature point at a time in the following + izip!( + repeat(conn.element(embedded_tet_mesh.vertices()).unwrap()), + tetrahedron_quadrature.weights(), + tetrahedron_quadrature.points() + ) + }) + .map(|(tet_element, w_tet, xi_tet)| { + // Map points and weights in reference element for the tetrahedron to the + // reference element of the background element + let x = tet_element.map_reference_coords(xi_tet); + let j_tet = tet_element.reference_jacobian(xi_tet); + let xi_bg = map_physical_coordinates(bg_element, &Point3::from(x)).expect("TODO: Handle error"); + let j_bg = bg_element.reference_jacobian(&xi_bg.coords); + + // Note: we assume that the background element is not completely degenerate here + debug_assert!(j_bg.determinant() != T::zero()); + let w_bg = *w_tet * j_tet.determinant().abs() / j_bg.determinant().abs(); + (w_bg, xi_bg.coords) + }) + .unzip(); + + if let Some(stabilization_options) = &quadrature_options.stabilization { + let factor = stabilization_options.stabilization_factor; + let (stab_w, stab_p) = &stabilization_options.stabilization_quadrature; + + w.extend(stab_w.iter().map(|w| factor * *w)); + p.extend(stab_p.iter().cloned()); + } + + Ok((w, p)) +} + +pub fn embed_quadrature_3d( + background_mesh: &Mesh3d, + embedding: &Embedding, + interior_quadrature: QuadraturePair3d, + embed_tet_quadrature_rule: (impl Sync + Quadrature), +) -> Result, Box> +where + T: RealField, + C: Sync + ElementConnectivity, + DefaultAllocator: ElementConnectivityAllocator, + DefaultAllocator: Allocator, + >::Buffer: Send + Sync, +{ + embed_quadrature_3d_with_opts( + background_mesh, + embedding, + interior_quadrature, + embed_tet_quadrature_rule, + &QuadratureOptions::default(), + ) +} + +pub fn embed_quadrature_3d_with_opts( + background_mesh: &Mesh3d, + embedding: &Embedding, + interior_quadrature: QuadraturePair3d, + embed_tet_quadrature_rule: (impl Sync + Quadrature), + quadrature_opts: &QuadratureOptions, +) -> Result, Box> +where + T: RealField, + C: Sync + ElementConnectivity, + DefaultAllocator: ElementConnectivityAllocator, + DefaultAllocator: Allocator, + >::Buffer: Send + Sync, +{ + let interface_quadratures = + compute_interface_quadrature_rules(background_mesh, embedding, embed_tet_quadrature_rule, quadrature_opts)?; + Ok(EmbeddedQuadrature::from_interior_and_interface( + interior_quadrature, + interface_quadratures, + )) +} + +/// Computes quadrature rules for interface elements in the background mesh. +/// +/// More precisely, it returns a vector of quadrature rules +pub fn compute_interface_quadrature_rules( + background_mesh: &Mesh3d, + embedding: &Embedding, + embed_tet_quadrature_rule: (impl Sync + Quadrature), + quadrature_opts: &QuadratureOptions, +) -> Result>, Box> +where + T: RealField, + C: Sync + ElementConnectivity, + DefaultAllocator: ElementConnectivityAllocator, + DefaultAllocator: Allocator, + >::Buffer: Send + Sync, +{ + let tet_quadrature = &embed_tet_quadrature_rule; + assert_eq!( + embedding.interface_cells.len(), + embedding.interface_cell_embeddings.len() + ); + + let result: Result, _> = embedding + .interface_cells + .par_iter() + .zip(embedding.interface_cell_embeddings.par_iter()) + .map(|(bg_cell_idx, embedded_intersection)| { + let element = background_mesh + .connectivity() + .get(*bg_cell_idx) + .ok_or_else(|| Box::::from("Invalid interface cell index."))? + .element(background_mesh.vertices()) + .unwrap(); + compute_element_embedded_quadrature(&element, embedded_intersection, tet_quadrature, quadrature_opts) + // We cannot pass through the error since we cannot (or don't want to) guarantee + // that the error implements Send + .map_err(|err| { + Box::::from(format!( + "Failed to construct embedded. quadrature for element.\ + Error description: {}", + err + )) + }) + }) + .collect(); + + result + // For some reason, dyn Error + Sync + Send does not automatically coerce + // to dyn Error in this case, so we need a simple cast + .map_err(|err| err as _) +} + +/// Constructs embedded model with catchall quadrature. +pub fn construct_embedded_model_3d( + background_mesh: &Mesh3d, + embedded_mesh: &PolyMesh3d, + interior_quadrature: QuadraturePair3d, + tet_quadrature: (impl Sync + Quadrature), +) -> Result, Box> +where + T: RealField, + C: Sync + ConnectivityMut + CellConnectivity + ElementConnectivity, + C::Cell: Send + BoundedGeometry + for<'a> ConvexPolyhedron<'a, T>, + DefaultAllocator: ElementConnectivityAllocator + Allocator, + >::Buffer: Send + Sync, +{ + construct_embedded_model_3d_with_opts( + background_mesh, + embedded_mesh, + interior_quadrature, + tet_quadrature, + &EmbedOptions::default(), + ) +} + +/// Constructs embedded model with catchall quadrature. +/// +/// TODO: Consider deprecating this? +pub fn construct_embedded_model_3d_with_opts( + background_mesh: &Mesh3d, + embedded_mesh: &PolyMesh3d, + interior_quadrature: QuadraturePair3d, + tet_quadrature: (impl Sync + Quadrature), + opts: &EmbedOptions, +) -> Result, Box> +where + T: RealField, + C: Sync + ConnectivityMut + CellConnectivity + ElementConnectivity, + C::Cell: Send + BoundedGeometry + for<'a> ConvexPolyhedron<'a, T>, + DefaultAllocator: ElementConnectivityAllocator + Allocator, + >::Buffer: Send + Sync, +{ + let embedding = embed_mesh_3d_with_opts(background_mesh, embedded_mesh, opts); + let interface_quadrature_rules = compute_interface_quadrature_rules( + background_mesh, + &embedding, + tet_quadrature, + &QuadratureOptions::default(), + ) + .map_err(|err| Box::::from(err))?; + + Ok(EmbeddedModelBuilder::from_embedding(background_mesh, embedding) + .catchall_quadrature(EmbeddedQuadrature::from_interior_and_interface( + interior_quadrature, + interface_quadrature_rules, + )) + .build()) +} diff --git a/fenris/src/embedding/mod.rs b/fenris/src/embedding/mod.rs new file mode 100644 index 0000000..a465a3d --- /dev/null +++ b/fenris/src/embedding/mod.rs @@ -0,0 +1,679 @@ +mod embedding2d; +mod embedding3d; +mod quadrature_reduction; + +pub use embedding2d::*; +pub use embedding3d::*; +pub use quadrature_reduction::*; + +use crate::allocators::{FiniteElementMatrixAllocator, VolumeFiniteElementAllocator}; +use crate::assembly::{color_nodes, ElementMatrixTransformation}; +use crate::connectivity::{CellConnectivity, Connectivity, ConnectivityMut}; +use crate::element::ElementConnectivity; +use crate::geometry::{Distance, DistanceQuery, GeometryCollection}; +use crate::mesh::{Mesh, Mesh3d}; +use crate::model::{FiniteElementInterpolator, MakeInterpolator}; +use crate::quadrature::QuadraturePair; +use crate::solid::assembly::{ + assemble_mass_into, assemble_pseudo_forces_into, assemble_pseudo_forces_into_par, assemble_stiffness_into, + assemble_stiffness_into_csr, assemble_transformed_stiffness_into_csr_par, assemble_transformed_stiffness_par, + ScalarMaterialSpaceFunction, +}; +use crate::solid::{ElasticMaterialModel, ElasticityModel, ElasticityModelParallel}; +use crate::{CooMatrix, CsrMatrix}; +use nalgebra::allocator::Allocator; +use nalgebra::{ + DVector, DVectorSlice, DVectorSliceMut, DefaultAllocator, DimMin, DimName, DimNameMul, Point, RealField, Scalar, + VectorN, U2, U3, +}; +use paradis::DisjointSubsets; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound(serialize = "T: Serialize,\ + Connectivity: Serialize,\ + >::Buffer: Serialize"))] +#[serde(bound(deserialize = "T: Deserialize<'de>,\ + Connectivity: Deserialize<'de>,\ + >::Buffer: Deserialize<'de>"))] +pub struct EmbeddedModel +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, +{ + // Store all interior connectivity first, then interface connectivity + background_mesh: Mesh, + // Number of interior connectivities + num_interior: usize, + + // Colors for parallel assembly + interior_colors: Vec, + interface_colors: Vec, + + mass_quadrature: Option>, + stiffness_quadrature: Option>, + elliptic_quadrature: Option>, + + mass_regularization_factor: T, +} + +pub type EmbeddedModel2d = EmbeddedModel; +pub type EmbeddedModel3d = EmbeddedModel; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound(serialize = "T: Serialize, VectorN: Serialize"))] +#[serde(bound(deserialize = "T: Deserialize<'de>, VectorN: Deserialize<'de>"))] +pub struct EmbeddedQuadrature +where + DefaultAllocator: Allocator, +{ + interior_quadrature: QuadraturePair, + // TODO: Use NestedVec? + interface_quadratures: Vec>, +} + +impl EmbeddedQuadrature +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, +{ + pub fn from_interior_and_interface( + interior_quadrature: QuadraturePair, + interface_quadratures: Vec>, + ) -> Self { + Self { + interior_quadrature, + interface_quadratures, + } + } + + pub fn interior_quadrature(&self) -> &QuadraturePair { + &self.interior_quadrature + } + + pub fn interface_quadratures(&self) -> &[QuadraturePair] { + &self.interface_quadratures + } +} + +impl EmbeddedModel +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, +{ + pub fn vertices(&self) -> &[Point] { + self.background_mesh.vertices() + } + + pub fn interior_connectivity(&self) -> &[Connectivity] { + &self.background_mesh.connectivity()[0..self.num_interior] + } + + pub fn interface_connectivity(&self) -> &[Connectivity] { + &self.background_mesh.connectivity()[self.num_interior..] + } + + pub fn background_mesh(&self) -> &Mesh { + &self.background_mesh + } + + pub fn set_mass_regularization_factor(&mut self, factor: T) { + self.mass_regularization_factor = factor; + } + + pub fn mass_quadrature(&self) -> Option<&EmbeddedQuadrature> { + self.mass_quadrature.as_ref() + } + + pub fn stiffness_quadrature(&self) -> Option<&EmbeddedQuadrature> { + self.stiffness_quadrature.as_ref() + } + + pub fn elliptic_quadrature(&self) -> Option<&EmbeddedQuadrature> { + self.elliptic_quadrature.as_ref() + } +} + +impl EmbeddedModel +where + T: RealField, + D: DimName + DimMin, + Connectivity: CellConnectivity + ElementConnectivity, + Connectivity::Cell: Distance>, + DefaultAllocator: VolumeFiniteElementAllocator, +{ + pub fn make_interpolator( + &self, + interpolation_points: &[Point], + ) -> Result, Box> { + FiniteElementInterpolator::interpolate_space(&self.background_mesh, interpolation_points) + } +} + +impl MakeInterpolator for EmbeddedModel +where + T: RealField, + D: DimName + DimMin, + Connectivity: CellConnectivity + ElementConnectivity, + Connectivity::Cell: Distance>, + DefaultAllocator: VolumeFiniteElementAllocator, +{ + fn make_interpolator( + &self, + interpolation_points: &[Point], + ) -> Result, Box> { + self.make_interpolator(interpolation_points) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EmbeddedModelBuilder +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, +{ + vertices: Option>>, + interior_connectivity: Option>, + interface_connectivity: Option>, + mass_quadrature: Option>, + stiffness_quadrature: Option>, + elliptic_quadrature: Option>, +} + +impl EmbeddedModelBuilder +where + T: RealField, + D: DimName, + C: Clone, + DefaultAllocator: Allocator, +{ + pub fn new() -> Self { + Self { + vertices: None, + interior_connectivity: None, + interface_connectivity: None, + mass_quadrature: None, + stiffness_quadrature: None, + elliptic_quadrature: None, + } + } + + pub fn vertices(&mut self, vertices: Vec>) -> &mut Self { + self.vertices = Some(vertices); + self + } + + pub fn interior_connectivity(&mut self, interior_connectivity: Vec) -> &mut Self { + self.interior_connectivity = Some(interior_connectivity); + self + } + + pub fn interface_connectivity(&mut self, interface_connectivity: Vec) -> &mut Self { + self.interface_connectivity = Some(interface_connectivity); + self + } + + pub fn mass_quadrature(&mut self, quadrature: EmbeddedQuadrature) -> &mut Self { + self.mass_quadrature = Some(quadrature); + self + } + + pub fn stiffness_quadrature(&mut self, quadrature: EmbeddedQuadrature) -> &mut Self { + self.stiffness_quadrature = Some(quadrature); + self + } + + pub fn elliptic_quadrature(&mut self, quadrature: EmbeddedQuadrature) -> &mut Self { + self.elliptic_quadrature = Some(quadrature); + self + } + + /// Sets all quadratures to the same quadrature. + pub fn catchall_quadrature(&mut self, quadrature: EmbeddedQuadrature) -> &mut Self { + self.mass_quadrature = Some(quadrature.clone()); + self.stiffness_quadrature = Some(quadrature.clone()); + self.elliptic_quadrature = Some(quadrature); + self + } + + pub fn build(&self) -> EmbeddedModel + where + C: Connectivity, + { + let interior_connectivity = self + .interior_connectivity + .clone() + .expect("Missing interior connectivity"); + let num_interior = interior_connectivity.len(); + let mut connectivity = interior_connectivity; + connectivity.extend( + self.interface_connectivity + .clone() + .expect("Missing interface connectivity."), + ); + let vertices = self.vertices.clone().expect("Missing vertices."); + + let background_mesh = Mesh::from_vertices_and_connectivity(vertices, connectivity); + + let interior_colors = { + let interior_connectivity = &background_mesh.connectivity()[0..num_interior]; + color_nodes(interior_connectivity) + }; + + let interface_colors = { + let interface_connectivity = &background_mesh.connectivity()[num_interior..]; + color_nodes(interface_connectivity) + }; + + EmbeddedModel { + background_mesh, + num_interior, + interior_colors, + interface_colors, + mass_quadrature: self.mass_quadrature.clone(), + stiffness_quadrature: self.stiffness_quadrature.clone(), + elliptic_quadrature: self.elliptic_quadrature.clone(), + mass_regularization_factor: T::zero(), + } + } +} + +impl EmbeddedModelBuilder +where + T: Scalar, + C: Clone + ConnectivityMut, + DefaultAllocator: Allocator, +{ + pub fn from_embedding(background_mesh: &Mesh3d, embedding: Embedding) -> Self { + // The embedding will mark some cells as exterior, which also means that some vertices + // might have no associated cells. To account for this, we reconstruct the background + // mesh with only the relevant connectivity, thereby removing unconnected vertices. + let num_interior = embedding.interior_cells.len(); + let mut keep_cells = embedding.interior_cells; + keep_cells.extend_from_slice(&embedding.interface_cells); + let new_background_mesh = background_mesh.keep_cells(&keep_cells); + let interior_connectivity = &new_background_mesh.connectivity()[0..num_interior]; + let interface_connectivity = &new_background_mesh.connectivity()[num_interior..]; + + // TODO: Store new background directly in builder instead of copying connectivity around + // (this is for legacy reasons) + + Self { + vertices: Some(new_background_mesh.vertices().to_vec()), + interior_connectivity: Some(interior_connectivity.to_vec()), + interface_connectivity: Some(interface_connectivity.to_vec()), + mass_quadrature: None, + stiffness_quadrature: None, + elliptic_quadrature: None, + } + } +} + +impl ElasticityModel for EmbeddedModel +where + T: RealField, + D: DimName + DimMin, + Connectivity: ElementConnectivity, + DefaultAllocator: FiniteElementMatrixAllocator, + D: DimNameMul, + Connectivity::NodalDim: DimNameMul, +{ + fn ndof(&self) -> usize { + D::dim() * self.vertices().len() + } + + fn assemble_stiffness_into( + &self, + csr: &mut CsrMatrix, + u: &DVector, + material_model: &dyn ElasticMaterialModel, + ) { + let error_msg = "Need stiffness quadrature for assembling stiffness matrix."; + assemble_stiffness_into_csr( + csr, + self.vertices(), + self.interior_connectivity(), + material_model, + u, + &|_| { + self.stiffness_quadrature + .as_ref() + .expect(&error_msg) + .interior_quadrature() + }, + ); + + assemble_stiffness_into_csr( + csr, + self.vertices(), + self.interface_connectivity(), + material_model, + u, + &|i| { + &self + .stiffness_quadrature + .as_ref() + .expect(&error_msg) + .interface_quadratures()[i] + }, + ); + } + + fn assemble_stiffness(&self, u: &DVector, material_model: &dyn ElasticMaterialModel) -> CooMatrix { + let ndof = self.ndof(); + let mut coo = CooMatrix::new(ndof, ndof); + + let error_msg = "Need stiffness quadrature for assembling stiffness matrix."; + + assemble_stiffness_into( + &mut coo, + self.vertices(), + self.interior_connectivity(), + material_model, + u, + &|_| { + self.stiffness_quadrature + .as_ref() + .expect(&error_msg) + .interior_quadrature() + }, + ); + + assemble_stiffness_into( + &mut coo, + self.vertices(), + self.interface_connectivity(), + material_model, + u, + &|i| { + &self + .stiffness_quadrature + .as_ref() + .expect(&error_msg) + .interface_quadratures()[i] + }, + ); + + coo + } + + fn assemble_mass(&self, density: T) -> CooMatrix { + let ndof = self.ndof(); + let mut coo = CooMatrix::new(ndof, ndof); + + let error_msg = "Need mass quadrature to assemble mass matrix."; + + assemble_mass_into( + &mut coo, + self.vertices(), + self.interior_connectivity(), + density, + &|_| { + self.mass_quadrature + .as_ref() + .expect(&error_msg) + .interior_quadrature() + }, + ); + + assemble_mass_into( + &mut coo, + self.vertices(), + self.interface_connectivity(), + density, + &|i| { + &self + .mass_quadrature + .as_ref() + .expect(&error_msg) + .interface_quadratures()[i] + }, + ); + + // TODO: Move this into the quadrature instead? + if self.mass_regularization_factor > T::zero() { + assemble_mass_into( + &mut coo, + self.vertices(), + self.background_mesh.connectivity(), + self.mass_regularization_factor * density, + &|_| { + self.mass_quadrature + .as_ref() + .expect(&error_msg) + .interior_quadrature() + }, + ); + } + + coo + } + + fn assemble_elastic_pseudo_forces( + &self, + u: DVectorSlice, + material_model: &dyn ElasticMaterialModel, + ) -> DVector { + let mut f = DVector::zeros(u.len()); + + let error_msg = "Need elliptic quadrature to assemble elastic pseudo forces."; + + assemble_pseudo_forces_into( + DVectorSliceMut::from(&mut f), + self.vertices(), + self.interior_connectivity(), + material_model, + u, + &|_| { + self.elliptic_quadrature + .as_ref() + .expect(&error_msg) + .interior_quadrature() + }, + ); + + assemble_pseudo_forces_into( + DVectorSliceMut::from(&mut f), + self.vertices(), + self.interface_connectivity(), + material_model, + u, + &|i| { + &self + .elliptic_quadrature + .as_ref() + .expect(&error_msg) + .interface_quadratures()[i] + }, + ); + + f + } + + fn compute_scalar_element_integrals( + &self, + _u: DVectorSlice, + _integrand: &dyn ScalarMaterialSpaceFunction, + ) -> DVector { + unimplemented!("Strain energy computation not implemented for EmbeddedModel"); + } +} + +impl ElasticityModelParallel for EmbeddedModel +where + T: RealField, + D: DimName + DimMin, + C: Sync + ElementConnectivity, + D: DimNameMul, + C::NodalDim: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, + >::Buffer: Sync, +{ + fn assemble_elastic_pseudo_forces_into_par( + &self, + mut f: DVectorSliceMut, + u: DVectorSlice, + material_model: &(dyn Sync + ElasticMaterialModel), + ) { + let error_msg = "Need elliptic quadrature to assemble pseudo forces."; + + assemble_pseudo_forces_into_par( + DVectorSliceMut::from(&mut f), + self.vertices(), + self.interior_connectivity(), + material_model, + u, + &|_| { + self.elliptic_quadrature + .as_ref() + .expect(&error_msg) + .interior_quadrature() + }, + &self.interior_colors, + ); + + assemble_pseudo_forces_into_par( + DVectorSliceMut::from(&mut f), + self.vertices(), + self.interface_connectivity(), + material_model, + u, + &|i| { + &self + .elliptic_quadrature + .as_ref() + .expect(&error_msg) + .interface_quadratures()[i] + }, + &self.interface_colors, + ); + } + + fn assemble_transformed_stiffness_par( + &self, + u: &DVector, + material_model: &(dyn Sync + ElasticMaterialModel), + transformation: &(dyn Sync + ElementMatrixTransformation), + ) -> CooMatrix { + let error_msg = "Need stiffness quadrature for assembling stiffness matrix."; + + let coo_interior = assemble_transformed_stiffness_par( + self.vertices(), + self.interior_connectivity(), + material_model, + u, + &|_| { + self.stiffness_quadrature + .as_ref() + .expect(&error_msg) + .interior_quadrature() + }, + transformation, + ); + + let coo_interface = assemble_transformed_stiffness_par( + self.vertices(), + self.interface_connectivity(), + material_model, + u, + &|i| { + &self + .stiffness_quadrature + .as_ref() + .expect(&error_msg) + .interface_quadratures()[i] + }, + transformation, + ); + + let mut coo = coo_interior; + coo += &coo_interface; + coo + } + + fn assemble_transformed_stiffness_into_par( + &self, + csr: &mut CsrMatrix, + u: &DVector, + material_model: &(dyn Sync + ElasticMaterialModel), + transformation: &(dyn Sync + ElementMatrixTransformation), + ) { + let error_msg = "Need stiffness quadrature for assembling stiffness matrix."; + + assemble_transformed_stiffness_into_csr_par( + csr, + self.vertices(), + self.interior_connectivity(), + material_model, + u, + &|_| { + self.stiffness_quadrature + .as_ref() + .expect(&error_msg) + .interior_quadrature() + }, + transformation, + &self.interior_colors, + ); + + assemble_transformed_stiffness_into_csr_par( + csr, + self.vertices(), + self.interface_connectivity(), + material_model, + u, + &|i| { + &self + .stiffness_quadrature + .as_ref() + .expect(&error_msg) + .interface_quadratures()[i] + }, + transformation, + &self.interface_colors, + ); + } + + fn compute_scalar_element_integrals_par( + &self, + _u: DVectorSlice, + _integrand: &(dyn Sync + ScalarMaterialSpaceFunction), + ) -> DVector { + unimplemented!("Strain energy computation not implemented for EmbeddedModel"); + } +} + +impl<'a, T, D, C> GeometryCollection<'a> for EmbeddedModel +where + T: Scalar, + D: DimName, + C: CellConnectivity, + DefaultAllocator: Allocator, +{ + type Geometry = C::Cell; + + fn num_geometries(&self) -> usize { + self.background_mesh.num_geometries() + } + + fn get_geometry(&'a self, index: usize) -> Option { + self.background_mesh.get_geometry(index) + } +} + +impl<'a, T, D, C, QueryGeometry> DistanceQuery<'a, QueryGeometry> for EmbeddedModel +where + T: RealField, + D: DimName, + C: CellConnectivity, + Mesh: DistanceQuery<'a, QueryGeometry>, + DefaultAllocator: Allocator, +{ + fn nearest(&'a self, query_geometry: &QueryGeometry) -> Option { + self.background_mesh.nearest(query_geometry) + } +} diff --git a/fenris/src/embedding/quadrature_reduction.rs b/fenris/src/embedding/quadrature_reduction.rs new file mode 100644 index 0000000..5fd1a26 --- /dev/null +++ b/fenris/src/embedding/quadrature_reduction.rs @@ -0,0 +1,363 @@ +use crate::embedding::{EmbeddedModel, EmbeddedQuadrature}; +use crate::geometry::AxisAlignedBoundingBox; +use crate::quadrature::{Quadrature, QuadraturePair}; +use itertools::Itertools; +use log::debug; +use nalgebra::allocator::Allocator; +use nalgebra::{DMatrix, DVector, DefaultAllocator, DimName, Point, RealField, Scalar, VectorN}; +use num::integer::binomial; +use numeric_literals::replace_float_literals; +use rayon::prelude::*; +use std::error::Error; +use std::fmt; +use std::fmt::Formatter; +use std::iter::repeat; + +#[replace_float_literals(T::from_f64(literal).unwrap())] +fn chebyshev_1d(x: T, n: usize) -> T +where + T: RealField, +{ + if n == 0 { + 1.0 + } else if n == 1 { + x + } else { + 2.0 * x * chebyshev_1d(x, n - 1) - chebyshev_1d(x, n - 2) + } +} + +struct LinearSystem { + matrix: DMatrix, + rhs: DVector, +} + +/// Compute the dimension of the space of polynomials in `d` variables with degree at most `n`. +/// +/// Assumes that `d >= 1`. Panics otherwise. +fn polynomial_space_dim(n: usize, d: usize) -> usize { + assert!(d >= 1, "d must be 1 or greater."); + (0..=n).map(|k| binomial(k + d - 1, k)).sum() +} + +#[replace_float_literals(T::from_f64(literal).unwrap())] +fn build_moment_fitting_system(weights: &[T], points: &[VectorN], strength: usize) -> LinearSystem +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator, +{ + // Determine scaling for basis functions to improve conditioning + // TODO: Fix this, it's stupid + let points_as_vectors: Vec<_> = points.iter().map(|p| Point::from(p.clone())).collect(); + let bounds = AxisAlignedBoundingBox::from_points(&points_as_vectors).expect("TODO: Handle case of no points"); + let center = bounds.center(); + let l = bounds.extents() / 2.0; + + // Alternative scaling based on average of point data below + // (some basic testing seems to suggest that the bounding box works just as well) + // // Scale basis functions to data + // let num_points = T::from_usize(points.len()).unwrap(); + // let center = points.iter().fold(Vector2::zeros(), |p1, p2| p1 + p2) / num_points; + // let l_x = points.iter() + // .map(|p| p.x) + // .fold(T::zero(), |l, x| T::max(l, (x - center.x).abs())); + // let l_y = points.iter() + // .map(|p| p.y) + // .fold(T::zero(), |l, y| T::max(l, (y - center.y).abs())); + + // alpha and beta are coefficients that scale and translate the 1d basis functions + // to better fit the problem data + // Note: We generally rescale by the size of the bounding box in each coordinate direction. + // However, if a direction is degenerate, we anyway cannot hope to obtain higher than + // 0-th order accuracy in that direction, so by setting the scale to zero + // we will effectively transform any non-constant basis function to a constant function + let alpha = l.map(|l_i| if l_i != T::zero() { T::one() / l_i } else { T::zero() }); + let beta = VectorN::repeat(T::one()) - alpha.component_mul(&(center.coords + l)); + + let mut matrix_elements = Vec::new(); + let mut rhs_elements = Vec::new(); + + // Loop over tensor products of axis dimensions i.e. + // (i, j) with i + j <= strength for 2D, + // (i, j, k) with i + j + k <= strength for 3D, + // TODO: lots of implicit allocations here + for orders in repeat(0..=strength) + .take(D::dim()) + .multi_cartesian_product() + { + if orders.iter().sum::() <= strength { + let polynomial = |point: &VectorN| { + let mut val = T::one(); + for d in 0..D::dim() { + val *= chebyshev_1d(alpha[d] * point[d] + beta[d], orders[d]) + } + val + }; + + // Evaluate matrix elements + let row_iter = points.iter().map(|point| polynomial(point)); + matrix_elements.extend(row_iter); + + let polynomial_integral: T = weights + .iter() + .cloned() + .zip(points.iter()) + .map(|(w, point)| w * polynomial(point)) + .fold(T::zero(), |sum, next_element| sum + next_element); + rhs_elements.push(polynomial_integral); + } + } + + let num_rows = rhs_elements.len(); + assert_eq!(matrix_elements.len() % num_rows, 0); + let num_cols = matrix_elements.len() / num_rows; + assert_eq!(num_rows, (polynomial_space_dim(strength, D::dim()))); + + LinearSystem { + matrix: DMatrix::from_row_slice(num_rows, num_cols, &matrix_elements), + rhs: DVector::from_column_slice(&rhs_elements), + } +} + +pub trait LpSolver { + /// Solve the designated LP, or return an error. + /// + /// The LP is described as follows. Find x that solves: + /// min c^T x + /// s.t. Ax = b + /// lb <= x <= ub + fn solve_lp( + &self, + c: &DVector, + a: &DMatrix, + b: &DVector, + lb: &[Option], + ub: &[Option], + ) -> Result, Box>; +} + +impl LpSolver for &X +where + T: Scalar, + X: LpSolver, +{ + fn solve_lp( + &self, + c: &DVector, + a: &DMatrix, + b: &DVector, + lb: &[Option], + ub: &[Option], + ) -> Result, Box> { + >::solve_lp(self, c, a, b, lb, ub) + } +} + +pub fn optimize_quadrature( + quadrature: impl Quadrature, + polynomial_strength: usize, + lp_solver: &impl LpSolver, +) -> Result, Box> +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator, +{ + let LinearSystem { matrix: p, rhs: _b } = + build_moment_fitting_system(quadrature.weights(), quadrature.points(), polynomial_strength); + + if quadrature.weights().len() <= p.nrows() { + // We can't improve the solution, so just return the current quadrature + Ok((quadrature.weights().to_vec(), quadrature.points().to_vec())) + } else { + // TODO: Consider recomposing matrix with SVD to remove small singular values, + // since depending on point distribution, the resulting system may be + // poorly conditioned + let lb = vec![Some(T::zero()); p.ncols()]; + let ub = vec![None; p.ncols()]; + + let w0 = DVector::from_column_slice(&quadrature.weights()); + + debug!("Number of weights in quadrature before simplification: {}", w0.len()); + debug!("Size of polynomial basis: {}", p.nrows()); + + // P w = P w0 is the original set of constraints. + // Take first r rows of V^T, where r is the rank of the matrix. V^T then + // is a basis for the null space of P. Thus we may replace Pw = P w0 + // with V^T w = V^T w0. Since V^T has orthogonal rows, it is much easier for the + // solver to work with. + let v_r_t = { + // TODO: Use more accurate SVD? + let threshold = T::default_epsilon(); + let p_svd = p.svd(false, true); + let max_svd = p_svd.singular_values.max(); + let idx_to_remove: Vec<_> = p_svd + .singular_values + .iter() + .cloned() + .enumerate() + .filter(|(_, val)| *val <= threshold * max_svd) + .map(|(i, _)| i) + .collect(); + p_svd.v_t.unwrap().remove_rows_at(&idx_to_remove) + }; + + // TODO: This should not print, we need a different mechanism to report this + if quadrature.weights().iter().any(|w_i| w_i < &T::zero()) { + eprintln!("Negative quadrature weights detected in optimize_quadrature()"); + } + + let b_v = &v_r_t * &w0; + let c = DVector::zeros(w0.len()); + + let w_bfp = lp_solver.solve_lp(&c, &v_r_t, &b_v, &lb, &ub)?; + + // TODO: Check residual etc??? + let (new_weights, new_points): (Vec<_>, Vec<_>) = w_bfp + .iter() + .copied() + .zip(quadrature.points().iter().cloned()) + // TODO: Enable threshold filtering...? + .filter(|(w, _)| w > &T::zero()) + .unzip(); + debug!("Number of weights in simplified quadrature: {}", new_weights.len()); + Ok((new_weights, new_points)) + } +} + +#[derive(Debug)] +pub struct QuadratureOptimizationError { + pub interface_connectivity_index: usize, + pub optimization_error: Box, +} + +impl fmt::Display for QuadratureOptimizationError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "Quadrature optimization error in interface connectivity {}. Error: {}", + self.interface_connectivity_index, self.optimization_error + ) + } +} + +impl EmbeddedQuadrature +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator, + VectorN: Sync + Send, +{ + pub fn simplified( + &self, + polynomial_strength: usize, + lp_solver: &(impl Sync + LpSolver), + ) -> Result { + simplify_quadrature(polynomial_strength, self, lp_solver) + } +} + +pub fn simplify_quadrature( + polynomial_strength: usize, + quadrature: &EmbeddedQuadrature, + lp_solver: &(impl Sync + LpSolver), +) -> Result, QuadratureOptimizationError> +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator, + VectorN: Sync + Send, +{ + let mut new_quadratures: Vec<_> = quadrature + .interface_quadratures() + .iter() + .map(|q| (q.weights().to_vec(), q.points().to_vec())) + .collect(); + + let solver = &lp_solver; + new_quadratures + .par_iter_mut() + .enumerate() + .map(|(idx, current_quadrature)| { + let new_quadrature = optimize_quadrature(&*current_quadrature, polynomial_strength, solver); + match new_quadrature { + Ok(new_quadrature) => { + *current_quadrature = new_quadrature; + Ok(()) + } + Err(err) => Err(QuadratureOptimizationError { + interface_connectivity_index: idx, + optimization_error: err, + }), + } + }) + .collect::, _>>()?; + + let interior_quadrature = ( + quadrature.interior_quadrature().weights().to_vec(), + quadrature.interior_quadrature().points().to_vec(), + ); + + Ok(EmbeddedQuadrature::from_interior_and_interface( + interior_quadrature, + new_quadratures, + )) +} + +impl Error for QuadratureOptimizationError {} + +impl EmbeddedModel +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator, + VectorN: Sync + Send, +{ + // TODO: Deprecate this + /// Optimizes *all* quadrature rules (mass, stiffness, elliptic, ...) to the same + /// polynomial strength. + /// + /// This is part of the legacy API and will be removed in the future. + pub fn optimize_quadrature( + &mut self, + polynomial_strength: usize, + lp_solver: &(impl Sync + LpSolver), + ) -> Result<(), QuadratureOptimizationError> { + let solver = &lp_solver; + let optimize_quadrature_rules = |rules: &mut Vec>| { + rules + .par_iter_mut() + .enumerate() + .map(|(idx, current_quadrature)| { + let new_quadrature = optimize_quadrature(&*current_quadrature, polynomial_strength, solver); + match new_quadrature { + Ok(new_quadrature) => { + *current_quadrature = new_quadrature; + Ok(()) + } + Err(err) => Err(QuadratureOptimizationError { + interface_connectivity_index: idx, + optimization_error: err, + }), + } + }) + .collect::, _>>() + .map(|_| ()) + }; + + if let Some(quadrature) = &mut self.mass_quadrature { + optimize_quadrature_rules(&mut quadrature.interface_quadratures)?; + } + + if let Some(quadrature) = &mut self.stiffness_quadrature { + optimize_quadrature_rules(&mut quadrature.interface_quadratures)?; + } + + if let Some(quadrature) = &mut self.elliptic_quadrature { + optimize_quadrature_rules(&mut quadrature.interface_quadratures)?; + } + + Ok(()) + } +} diff --git a/fenris/src/error.rs b/fenris/src/error.rs new file mode 100644 index 0000000..39064e9 --- /dev/null +++ b/fenris/src/error.rs @@ -0,0 +1,115 @@ +//! Functionality for error estimation. + +use crate::allocators::VolumeFiniteElementAllocator; +use crate::element::FiniteElement; +use crate::quadrature::{Quadrature, Quadrature2d}; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, DimMin, DimName, DimNameMul, MatrixMN, Point, RealField, VectorN, U1, U2}; + +/// Estimate the squared L^2 error of `u_h - u` on the given element with the given basis +/// weights and quadrature points. +/// +/// `u(x, i)` represents the value of `u` at physical coordinate `x`. `i` is the index of the +/// quadrature point. +/// +/// More precisely, estimate the integral of `dot(u_h - u, u_h - u)`, where `u_h = u_i N_i`, +/// with `u_i` the `i`-th column in `u` denoting the `m`-dimensional weight associated with node `i`, +/// and `N_i` is the basis function associated with node `i`. +#[allow(non_snake_case)] +pub fn estimate_element_L2_error_squared( + element: &Element, + u: impl Fn(&Point, usize) -> VectorN, + u_weights: &MatrixMN, + quadrature: impl Quadrature, +) -> T +where + T: RealField, + Element: FiniteElement, + GeometryDim: DimName + DimMin, + SolutionDim: DimNameMul, + DefaultAllocator: VolumeFiniteElementAllocator + + Allocator + + Allocator, +{ + let weights = quadrature.weights(); + let points = quadrature.points(); + + use itertools::izip; + + let mut result = T::zero(); + for (i, (w, xi)) in izip!(weights, points).enumerate() { + let x = element.map_reference_coords(xi); + let j = element.reference_jacobian(xi); + let g = element.evaluate_basis(xi); + let u_h = u_weights * g.transpose(); + let u_at_x = u(&Point::from(x), i); + let error = u_h - u_at_x; + let error2 = error.dot(&error); + result += error2 * *w * j.determinant().abs(); + } + result +} + +#[allow(non_snake_case)] +pub fn estimate_element_L2_error( + element: &Element, + u: impl Fn(&Point, usize) -> VectorN, + u_weights: &MatrixMN, + quadrature: impl Quadrature, +) -> T +where + T: RealField, + Element: FiniteElement, + SolutionDim: DimNameMul, + GeometryDim: DimName + DimMin, + DefaultAllocator: VolumeFiniteElementAllocator + + Allocator + + Allocator, +{ + estimate_element_L2_error_squared(element, u, u_weights, quadrature).sqrt() +} + +/// Estimate the squared L^2 norm on the given element with the given basis weights and quadrature +/// points. +/// +/// More precisely, compute the integral of `dot(u_h, u_h)`, where `u_h = u_i N_i`, with `u_i`, +/// the `i`-th column in `u`, denoting the `m`-dimensional weight associated with node `i`, +/// and `N_i` is the basis function associated with node `i`. +#[allow(non_snake_case)] +pub fn estimate_element_L2_norm_squared( + element: &Element, + u_weights: &MatrixMN, + quadrature: impl Quadrature2d, +) -> T +where + T: RealField, + Element: FiniteElement, + SolutionDim: DimNameMul, + DefaultAllocator: VolumeFiniteElementAllocator + + Allocator + + Allocator, +{ + estimate_element_L2_error_squared( + element, + |_, _| VectorN::::repeat(T::zero()), + u_weights, + quadrature, + ) +} + +#[allow(non_snake_case)] +pub fn estimate_element_L2_norm( + element: &Element, + u: &MatrixMN, + quadrature: impl Quadrature2d, +) -> T +where + T: RealField, + Element: FiniteElement, + SolutionDim: DimNameMul, + DefaultAllocator: VolumeFiniteElementAllocator + + Allocator + + Allocator, +{ + estimate_element_L2_norm_squared(element, u, quadrature).sqrt() +} diff --git a/fenris/src/geometry/mod.rs b/fenris/src/geometry/mod.rs new file mode 100644 index 0000000..30b3062 --- /dev/null +++ b/fenris/src/geometry/mod.rs @@ -0,0 +1,1258 @@ +mod polytope; +use itertools::izip; +use nalgebra::{ + distance_squared, DefaultAllocator, DimName, Point, Point2, Point3, RealField, Scalar, Unit, Vector3, VectorN, U2, + U3, +}; +pub use polytope::*; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; + +mod polygon; +pub use polygon::*; + +use nalgebra::allocator::Allocator; +use numeric_literals::replace_float_literals; +use std::fmt::Debug; + +pub mod polymesh; +pub mod procedural; +pub mod sdf; +pub mod vtk; + +#[cfg(feature = "proptest")] +pub mod proptest_strategies; + +pub trait BoundedGeometry +where + T: Scalar, + DefaultAllocator: Allocator, +{ + type Dimension: DimName; + + fn bounding_box(&self) -> AxisAlignedBoundingBox; +} + +pub trait Distance +where + T: Scalar, +{ + /// Returns an interval `[l, u]` for the distance `d`, such that `d` is contained in `[l, u]`. + fn distance_bound(&self, query_geometry: &QueryGeometry) -> [T; 2] { + let d = self.distance(query_geometry); + [d.clone(), d] + } + + fn distance(&self, query_geometry: &QueryGeometry) -> T; +} + +pub trait Overlaps +where + T: Scalar, +{ + fn overlaps_with(&self, query_geometry: &QueryGeometry) -> bool; +} + +pub trait GeometryCollection<'a> { + type Geometry; + + fn num_geometries(&self) -> usize; + fn get_geometry(&'a self, index: usize) -> Option; +} + +pub trait DistanceQuery<'a, QueryGeometry>: GeometryCollection<'a> { + // type KNearestIter: Iterator; + + // fn k_nearest(&'a self, query_geometry: &'a QueryGeometry, k: usize) -> Self::KNearestIter; + + fn nearest(&'a self, query_geometry: &QueryGeometry) -> Option; +} + +pub trait NeighborhoodQuery<'a, QueryGeometry>: GeometryCollection<'a> { + type NeighborsIter: Iterator; + + fn neighbors_within_distance(&'a self, query_geometry: &'a QueryGeometry) -> Self::NeighborsIter; +} + +pub trait OverlapQuery<'a, QueryGeometry>: GeometryCollection<'a> { + type OverlapIter: Iterator; + + fn overlapping_geometries(&'a self, query_geometry: &'a QueryGeometry) -> Self::OverlapIter; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SignedDistanceResult +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, +{ + pub feature_id: usize, + pub point: Point, + pub signed_distance: T, +} + +pub trait SignedDistance +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, +{ + fn query_signed_distance(&self, point: &Point) -> Option>; +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound( + serialize = "VectorN: Serialize", + deserialize = "VectorN: Deserialize<'de>" +))] +pub struct AxisAlignedBoundingBox +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, +{ + min: VectorN, + max: VectorN, +} + +impl Copy for AxisAlignedBoundingBox +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, + VectorN: Copy, + // >::Buffer: Copy, +{ +} + +pub type AxisAlignedBoundingBox2d = AxisAlignedBoundingBox; +pub type AxisAlignedBoundingBox3d = AxisAlignedBoundingBox; + +impl AxisAlignedBoundingBox +where + T: Scalar + PartialOrd, + D: DimName, + DefaultAllocator: Allocator, +{ + pub fn new(min: VectorN, max: VectorN) -> Self { + for i in 0..D::dim() { + assert!(min[i] <= max[i]); + } + Self { min, max } + } + + pub fn min(&self) -> &VectorN { + &self.min + } + + pub fn max(&self) -> &VectorN { + &self.max + } +} + +impl From> for AxisAlignedBoundingBox +where + T: Scalar + PartialOrd, + D: DimName, + DefaultAllocator: Allocator, +{ + fn from(point: Point) -> Self { + AxisAlignedBoundingBox::new(point.coords.clone(), point.coords) + } +} + +impl AxisAlignedBoundingBox +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator, +{ + /// Computes the minimal bounding box which encloses both `this` and `other`. + pub fn enclose(&self, other: &AxisAlignedBoundingBox) -> Self { + let min = self.min.iter().zip(&other.min).map(|(a, b)| T::min(*a, *b)); + let min = VectorN::::from_iterator(min); + + let max = self.max.iter().zip(&other.max).map(|(a, b)| T::max(*a, *b)); + let max = VectorN::::from_iterator(max); + + AxisAlignedBoundingBox::new(min, max) + } + + pub fn from_points<'a>(points: impl IntoIterator>) -> Option { + let mut points = points.into_iter(); + points.next().map(|first_point| { + points.fold(AxisAlignedBoundingBox::from(first_point.clone()), |aabb, point| { + aabb.enclose(&AxisAlignedBoundingBox::from(point.clone())) + }) + }) + } + + pub fn extents(&self) -> VectorN { + self.max() - self.min() + } + + pub fn max_extent(&self) -> T { + (self.max() - self.min()).amax() + } + + pub fn center(&self) -> Point { + Point::from((self.max() + self.min()) / T::from_f64(2.0).unwrap()) + } + + pub fn uniformly_scale(&self, scale: T) -> Self { + Self { + min: &self.min * scale, + max: &self.max * scale, + } + } + + pub fn contains_point(&self, point: &Point) -> bool { + (0..D::dim()).all(|dim| point[dim] > self.min[dim] && point[dim] < self.max[dim]) + } +} + +impl BoundedGeometry for AxisAlignedBoundingBox +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator, +{ + type Dimension = D; + + fn bounding_box(&self) -> AxisAlignedBoundingBox { + self.clone() + } +} + +#[derive(Copy, Debug, Clone, PartialEq, Eq)] +pub enum Orientation { + Clockwise, + Counterclockwise, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound(serialize = "Point: Serialize", deserialize = "Point: Deserialize<'de>"))] +pub struct Triangle(pub [Point; 3]) +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator; + +impl Copy for Triangle +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, + Point: Copy, +{ +} + +/// A triangle in two dimensions, consisting of three vertices. +/// +/// For most purposes, the triangle is assumed to be specified with a counter-clockwise +/// winding order, but it also provides facilities for determining the winding order +/// of an arbitrarily constructed triangle. +pub type Triangle2d = Triangle; +pub type Triangle3d = Triangle; + +impl Triangle +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, +{ + pub fn swap_vertices(&mut self, i: usize, j: usize) { + self.0.swap(i, j); + } +} + +impl Triangle +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator, +{ + pub fn centroid(&self) -> Point { + let mut centroid = VectorN::zeros(); + for p in &self.0 { + centroid += &p.coords * T::from_f64(1.0 / 3.0).unwrap(); + } + Point::from(centroid) + } + + /// Returns an array of vectors corresponding to the three sides of the triangle. + pub fn sides(&self) -> [VectorN; 3] { + let a = &self.0[0]; + let b = &self.0[1]; + let c = &self.0[2]; + [b - a, c - b, a - c] + } +} + +impl Triangle2d +where + T: RealField, +{ + pub fn orientation(&self) -> Orientation { + if self.signed_area() >= T::zero() { + Orientation::Clockwise + } else { + Orientation::Counterclockwise + } + } + + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T."))] + pub fn signed_area(&self) -> T { + let a = &self.0[0]; + let b = &self.0[1]; + let c = &self.0[2]; + let ab = b - a; + let ac = c - a; + 0.5 * ab.perp(&ac) + } + + pub fn area(&self) -> T { + self.signed_area().abs() + } +} + +impl Triangle3d +where + T: RealField, +{ + /// Returns a vector normal to the triangle. The vector is *not* normalized. + pub fn normal_dir(&self) -> Vector3 { + let a = &self.0[0]; + let b = &self.0[1]; + let c = &self.0[2]; + let ab = b - a; + let ac = c - a; + let n = ab.cross(&ac); + n + } + + pub fn normal(&self) -> Vector3 { + self.normal_dir().normalize() + } + + /// TODO: Remove this. It makes no sense for 3D. + pub fn orientation(&self) -> Orientation { + if self.signed_area() >= T::zero() { + Orientation::Clockwise + } else { + Orientation::Counterclockwise + } + } + + /// TODO: Remove this. It makes no sense for 3D (moreover, the current implementation + /// gives the non-negative ara in any case). + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T."))] + pub fn signed_area(&self) -> T { + let a = &self.0[0]; + let b = &self.0[1]; + let c = &self.0[2]; + let ab = b - a; + let ac = c - a; + 0.5 * ab.cross(&ac).norm() + } + + pub fn area(&self) -> T { + self.signed_area().abs() + } + + /// Determines the orientation of a point with respect to the plane containing the triangle. + /// + /// This is the common "orientation test" used in computational geometry. The test returns + /// a value whose sign is the same as `dot(n, x - x0)`, where `x0` is the projection of + /// the point onto the triangle plane. + /// + /// Note that at the moment, this implementation is **NOT** robust (in the sense of exact/robust + /// geometric predicates). + pub fn point_orientation(&self, point: &Point3) -> OrientationTestResult { + // Note: This is by no means robust in the sense of floating point accuracy. + let point_in_plane = &self.0[0]; + + let x = point; + let x0 = point_in_plane; + let n = self.normal_dir().normalize(); + let projected_dist = n.dot(&(x - x0)); + + if projected_dist > T::zero() { + OrientationTestResult::Positive + } else if projected_dist < T::zero() { + OrientationTestResult::Negative + } else { + OrientationTestResult::Zero + } + } +} + +impl BoundedGeometry for Triangle +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator, +{ + type Dimension = D; + + fn bounding_box(&self) -> AxisAlignedBoundingBox { + AxisAlignedBoundingBox::from_points(&self.0).unwrap() + } +} + +impl Distance> for Triangle2d +where + T: RealField, +{ + fn distance(&self, point: &Point2) -> T { + let sdf = self.query_signed_distance(point).unwrap(); + T::max(T::zero(), sdf.signed_distance) + } +} + +impl SignedDistance for Triangle2d +where + T: RealField, +{ + fn query_signed_distance(&self, point: &Point2) -> Option> { + // TODO: This is not the most efficient way to compute this + let mut inside = true; + + let mut closest_segment = 0; + let mut closest_dist2 = T::max_value(); + let mut closest_point = Point2::origin(); + + // We assume counterclockwise orientation. + for i in 0..3 { + let a = &self.0[i]; + let b = &self.0[(i + 1) % 3]; + let segment = LineSegment2d::new(*a, *b); + // Normal point outwards, i.e. towards the "right" + let normal_dir = segment.normal_dir(); + let projected_point = segment.closest_point(point); + let d = &point.coords - &projected_point.coords; + + if d.dot(&normal_dir) > T::zero() { + inside = false; + } + + let dist2 = d.magnitude_squared(); + if dist2 < closest_dist2 { + closest_segment = i; + closest_dist2 = dist2; + closest_point = projected_point; + } + } + + let sign = if inside { T::from_f64(-1.0).unwrap() } else { T::one() }; + + Some(SignedDistanceResult { + feature_id: closest_segment, + point: closest_point, + signed_distance: sign * closest_dist2.sqrt(), + }) + } +} + +impl Distance> for Triangle3d +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).unwrap())] + fn distance(&self, point: &Point) -> T { + self.project_point(point).distance + } +} + +impl<'a, T: RealField> ConvexPolygon3d<'a, T> for Triangle3d { + fn num_vertices(&self) -> usize { + 3 + } + + fn get_vertex(&self, index: usize) -> Option> { + self.0.get(index).copied() + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum OrientationTestResult { + Positive, + Zero, + Negative, +} + +#[derive(Debug, Copy, Clone, PartialEq, Hash, Serialize, Deserialize)] +pub struct Tetrahedron +where + T: Scalar, +{ + // Ordering uses same conventions as Tet4Connectivity + vertices: [Point3; 4], +} + +impl Tetrahedron +where + T: Scalar, +{ + /// Construct tetrahedron from the given points. + /// + /// Ordering is the same as for `Tet4Connectivity`. + pub fn from_vertices(vertices: [Point3; 4]) -> Self { + Self { vertices } + } +} + +impl Tetrahedron +where + T: RealField, +{ + /// Reference tetrahedron. + #[replace_float_literals(T::from_f64(literal).unwrap())] + pub fn reference() -> Self { + Self::from_vertices([ + Point3::new(-1.0, -1.0, -1.0), + Point3::new(1.0, -1.0, -1.0), + Point3::new(-1.0, 1.0, -1.0), + Point3::new(-1.0, -1.0, 1.0), + ]) + } +} + +impl BoundedGeometry for Tetrahedron { + type Dimension = U3; + + fn bounding_box(&self) -> AxisAlignedBoundingBox { + AxisAlignedBoundingBox::from_points(&self.vertices).unwrap() + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Hash)] +pub struct Hexahedron +where + T: Scalar, +{ + // Ordering uses same conventions as Hex8Connectivity + vertices: [Point3; 8], +} + +impl BoundedGeometry for Hexahedron +where + T: RealField, +{ + type Dimension = U3; + + fn bounding_box(&self) -> AxisAlignedBoundingBox { + AxisAlignedBoundingBox::from_points(&self.vertices).unwrap() + } +} + +impl Hexahedron +where + T: Scalar, +{ + pub fn from_vertices(vertices: [Point3; 8]) -> Self { + Self { vertices } + } +} + +impl Hexahedron +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).unwrap())] + pub fn reference() -> Self { + Self::from_vertices([ + Point3::new(-1.0, -1.0, -1.0), + Point3::new(1.0, -1.0, -1.0), + Point3::new(1.0, 1.0, -1.0), + Point3::new(-1.0, 1.0, -1.0), + Point3::new(-1.0, -1.0, 1.0), + Point3::new(1.0, -1.0, 1.0), + Point3::new(1.0, 1.0, 1.0), + Point3::new(-1.0, 1.0, 1.0), + ]) + } +} + +impl Distance> for Hexahedron +where + T: RealField, +{ + fn distance(&self, point: &Point3) -> T { + let signed_dist = self.compute_signed_distance(point).signed_distance; + T::max(signed_dist, T::zero()) + } +} + +impl SignedDistance for Hexahedron +where + T: RealField, +{ + fn query_signed_distance(&self, point: &Point) -> Option> { + Some(self.compute_signed_distance(point)) + } +} + +impl<'a, T> ConvexPolyhedron<'a, T> for Hexahedron +where + T: RealField, +{ + type Face = Quad3d; + + fn num_faces(&self) -> usize { + 6 + } + + fn get_face(&self, index: usize) -> Option { + let v = &self.vertices; + let quad = |i, j, k, l| Some(Quad3d::from_vertices([v[i], v[j], v[k], v[l]])); + + // Must choose faces carefully so that they point towards the interior + match index { + 0 => quad(0, 1, 2, 3), + 1 => quad(4, 5, 1, 0), + 2 => quad(5, 6, 2, 1), + 3 => quad(6, 7, 3, 2), + 4 => quad(0, 3, 7, 4), + 5 => quad(4, 7, 6, 5), + _ => None, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Quad3d { + vertices: [Point3; 4], +} + +impl Quad3d { + pub fn from_vertices(vertices: [Point3; 4]) -> Self { + Self { vertices } + } +} + +impl<'a, T> ConvexPolygon3d<'a, T> for Quad3d +where + T: Scalar, +{ + fn num_vertices(&self) -> usize { + 4 + } + + fn get_vertex(&self, index: usize) -> Option> { + self.vertices.get(index).cloned() + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct HalfSpace { + point: Point3, + normal: Unit>, +} + +impl HalfSpace +where + T: RealField, +{ + pub fn contains_point(&self, point: &Point3) -> bool { + let x = point.coords; + let x0 = self.point.coords; + self.normal.dot(&(x - x0)) >= T::zero() + } + + pub fn from_point_and_normal(point: Point3, normal: Unit>) -> Self { + Self { point, normal } + } + + pub fn plane(&self) -> Plane3d { + Plane3d::from_point_and_normal(self.point, self.normal) + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Plane3d { + point: Point3, + normal: Unit>, +} + +impl Plane3d +where + T: RealField, +{ + pub fn normal(&self) -> &Unit> { + &self.normal + } + + pub fn point(&self) -> &Point3 { + &self.point + } + + pub fn from_point_and_normal(point: Point3, normal: Unit>) -> Self { + Self { point, normal } + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct LineSegment3d { + end_points: [Point3; 2], +} + +impl LineSegment3d { + pub fn from_end_points(end_points: [Point3; 2]) -> Self { + Self { end_points } + } +} + +impl LineSegment3d { + pub fn project_point_parametric(&self, point: &Point3) -> T { + let a = self.end_points[0].coords; + let b = self.end_points[1].coords; + let d = &b - &a; + let d2 = d.magnitude_squared(); + if d2 == T::zero() { + // If the endpoints are the same, the segment collapses to a single point, + // in which case e.g. t == 0 gives the correct solution. + T::zero() + } else { + let x = point.coords; + let t = (x - &a).dot(&d) / d2; + t + } + } + + pub fn project_point(&self, point: &Point3) -> Point3 { + let t = self.project_point_parametric(point); + if t <= T::zero() { + self.end_points[0] + } else if t >= T::one() { + self.end_points[1] + } else { + self.point_from_parameter(t) + } + } + + pub fn point_from_parameter(&self, t: T) -> Point3 { + let a = self.end_points[0]; + let b = self.end_points[1]; + Point3::from(a.coords * (T::one() - t) + &b.coords * t) + } + + #[allow(non_snake_case)] + pub fn closest_point_to_plane_parametric(&self, plane: &Plane3d) -> T { + let n = plane.normal(); + let x0 = plane.point(); + let [a, b] = &self.end_points; + let d = &b.coords - &a.coords; + let y = &x0.coords - &a.coords; + + let nTd = n.dot(&d); + let nTy = n.dot(&y); + + // The parameter t is generally given by the equation + // dot(n, d) * t = dot(n, y) + // but we must be careful, since dot(n, d) can get arbitrarily close to 0, + // which causes some challenges. + let t = if nTd.signum() == nTy.signum() { + // Sign is the same, thus t >= 0 + if nTy.abs() >= nTd.abs() { + T::one() + } else { + nTy / nTd + } + } else { + // t must be negative, directly clamp to zero + T::zero() + }; + + t + } + + pub fn closest_point_to_plane(&self, plane: &Plane3d) -> Point3 { + let t = self.closest_point_to_plane_parametric(plane); + self.point_from_parameter(t) + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct PolygonPointProjection3d { + /// The projection of the point onto the polygon. + pub projected_point: Point3, + /// The (absolute) distance from the point to the projected point. + pub distance: T, + /// The signed distance from the point to the polygon plane. The sign is positive + /// if the vector from the plane projection to the point is in the direction of the polygon + /// normal. + pub signed_plane_distance: T, +} + +/// A convex polygon in 3D. +/// +/// Vertices are assumed to be ordered counter-clockwise. +pub trait ConvexPolygon3d<'a, T: Scalar>: Debug { + fn num_vertices(&self) -> usize; + fn get_vertex(&self, index: usize) -> Option>; + + fn compute_half_space(&self) -> Option> + where + T: RealField, + { + let normal = self.compute_face_normal(); + let point = self.get_vertex(0)?; + Some(HalfSpace::from_point_and_normal(point, Unit::new_unchecked(normal))) + } + + /// Computes a vector normal to the polygon (oriented outwards w.r.t. a counter-clockwise + /// orientation), whose absolute magnitude is the area of the polygon. + /// + /// Note that if the polygon is non-planar or not properly oriented, there are no + /// guarantees on the quality of the result. + #[replace_float_literals(T::from_f64(literal).unwrap())] + fn compute_area_vector(&self) -> Vector3 + where + T: RealField, + { + assert!(self.num_vertices() >= 3, "Polygons must have at least 3 vertices."); + + let mut area_vector = Vector3::zeros(); + + let a = self.get_vertex(0).unwrap(); + for i in 1..self.num_vertices() - 1 { + let b = self.get_vertex(i).unwrap(); + let c = self.get_vertex(i + 1).unwrap(); + let ab = &b.coords - &a.coords; + let ac = &c.coords - &a.coords; + area_vector += ab.cross(&ac) * 0.5; + } + + area_vector + } + + fn compute_face_normal(&self) -> Vector3 + where + T: RealField, + { + // In principle, we could compute the face normal simply by + // taking the cross product of the first two segments. However, the first M segments + // may all be co-linear, in which case this will fail. Of course, *all* segments + // may be co-linear, in which we can't do anything about it. + // + // To do this robustly, we instead consider a triangle fan starting in the first vertex. + // For each triangle ABC, we compute the quantity AB x AC. Taking the sum of all these + // quantities and finally normalizing it should give a fairly reliable quantity, + // given that the polygon itself is not degenerate. + + self.compute_area_vector().normalize() + } + + fn project_point(&self, point: &Point3) -> PolygonPointProjection3d + where + T: RealField, + { + assert!(self.num_vertices() >= 3, "Polygon must have at least 3 vertices."); + + // First, "extrude" the polygon by extruding each edge perpendicular to the + // face. Then check if the point is contained in this extruded prism + // by checking it against all half-spaces defined by the extruded edges. + let n = self.compute_face_normal(); + + let mut inside = true; + + for i in 0..self.num_vertices() { + let v1 = self.get_vertex(i).unwrap(); + let v2 = self.get_vertex((i + 1) % self.num_vertices()).unwrap(); + + // Vector parallel to edge + let e = &v2.coords - &v1.coords; + + // Half space normal points towards the interior of the polygon + let half_space_normal = Unit::new_normalize(-e.cross(&n)); + let half_space = HalfSpace::from_point_and_normal(v1, half_space_normal); + + if !half_space.contains_point(point) { + inside = false; + break; + } + } + + if inside { + // Point is contained inside the extruded prism, so the projection onto the + // polygon is simply a projection onto the polygon plane. + + // Pick any point in the polygon plane + let x0 = self.get_vertex(0).unwrap(); + let signed_plane_distance = n.dot(&(point - x0)); + let projected_point = point - &n * signed_plane_distance; + PolygonPointProjection3d { + projected_point, + signed_plane_distance, + // the projected point is equal to the projection onto the plane + distance: signed_plane_distance.abs(), + } + } else { + // Point is *not* contained inside the extruded prism. Thus we must pick the + // closest point on any of the edges of the polygon. + + let mut closest_dist2 = T::max_value(); + let mut closest_point = Point3::origin(); + + for i in 0..self.num_vertices() { + let v1 = self.get_vertex(i).unwrap(); + let v2 = self.get_vertex((i + 1) % self.num_vertices()).unwrap(); + let segment = LineSegment3d::from_end_points([v1, v2]); + let projected = segment.project_point(point); + let dist2 = distance_squared(&projected, point); + + if dist2 < closest_dist2 { + closest_dist2 = dist2; + closest_point = projected; + } + } + + let signed_plane_distance = n.dot(&(point - &closest_point)); + PolygonPointProjection3d { + projected_point: closest_point, + signed_plane_distance, + distance: closest_dist2.sqrt(), + } + } + } +} + +pub trait ConvexPolyhedron<'a, T: Scalar>: Debug { + type Face: ConvexPolygon3d<'a, T>; + + fn num_faces(&self) -> usize; + fn get_face(&self, index: usize) -> Option; + + #[replace_float_literals(T::from_f64(literal).unwrap())] + fn compute_signed_distance(&self, point: &Point3) -> SignedDistanceResult + where + T: RealField, + { + assert!(self.num_faces() >= 4, "Polyhedron must have at least 4 faces."); + let mut inside = true; + let mut closest_dist = T::max_value(); + let mut closest_point = Point3::origin(); + let mut closest_face_index = 0; + + for i in 0..self.num_faces() { + let face = self.get_face(i).unwrap(); + let projection = face.project_point(point); + + if projection.distance < closest_dist { + closest_dist = projection.distance; + closest_face_index = i; + closest_point = projection.projected_point; + } + + // If the point is outside any of the half-spaces defined by the faces, + // the point must be outside the polyhedron + if projection.signed_plane_distance < T::zero() { + inside = false; + } + } + + let sign = if inside { -1.0 } else { 1.0 }; + + debug_assert!(closest_dist >= 0.0); + SignedDistanceResult { + feature_id: closest_face_index, + point: closest_point, + signed_distance: sign * closest_dist, + } + } + + fn compute_volume(&'a self) -> T + where + T: RealField, + { + let faces = (0..self.num_faces()).map(move |idx| { + self.get_face(idx) + .expect("Number of faces reported must be correct.") + }); + compute_polyhedron_volume_from_faces(faces) + } + + /// Check if this polyhedron contains the given point. + /// + /// TODO: Write tests + fn contains_point(&'a self, point: &Point3) -> bool + where + T: RealField, + { + // The convex polyhedron contains the point if all half-spaces associated with + // faces contain the point + for i in 0..self.num_faces() { + let face = self.get_face(i).unwrap(); + let half_space = face + .compute_half_space() + .expect("TODO: What to do if we cannot compute half space?"); + + if !half_space.contains_point(point) { + return false; + } + } + + true + } +} + +#[replace_float_literals(T::from_f64(literal).unwrap())] +pub fn compute_polyhedron_volume_from_faces<'a, T, F>(boundary_faces: impl 'a + IntoIterator) -> T +where + T: RealField, + F: ConvexPolygon3d<'a, T>, +{ + // We use the formula given on the Wikipedia page for Polyhedra: + // https://en.wikipedia.org/wiki/Polyhedron#Volume + + let mut volume = T::zero(); + for face in boundary_faces { + // Ignore degenerate faces consisting of zero vertices + // TODO: Handle this different somehow? It's a little inconsistent what we + // require in various methods + if face.num_vertices() > 2 { + let x0 = face.get_vertex(0).unwrap(); + // TODO: Be consistent about what direction normal should have! + let area_vector = face.compute_area_vector(); + let area = area_vector.magnitude(); + if area > T::zero() { + let normal = area_vector.normalize(); + volume += (normal.dot(&x0.coords)) * area; + } + } + } + + // The absolute value should negate issues caused by flipped normals, + // as long as the normals are consistently oriented + volume = volume.abs() / 3.0; + + volume +} + +// TODO: Actually implement SignedDistance for Tetrahedron +//impl SignedDistance for Tetrahedron +//where +// T: RealField +//{ +// #[replace_float_literals(T::from_f64(literal).unwrap())] +// fn query_signed_distance(&self, point: &Point3) -> Option> { +// let triangle = |i, j, k| Triangle([self.vertices[i], self.vertices[j], self.vertices[k]]); +// +// let tri_faces = [ +// // We must carefully choose the ordering of vertices so that the +// // resulting faces have outwards-pointing normals +// triangle(2, 1, 0), +// triangle(1, 2, 3), +// triangle(0, 1, 3), +// triangle(2, 0, 3) +// ]; +// +// let mut point_inside = true; +// let mut min_dist = T::max_value(); +// +// for tri_face in &tri_faces { +// // Remember that the triangles are oriented such that *outwards* is the positive +// // direction, so for the point to be inside of the cell, we need its orientation +// // with respect to each face to be *negative* +// if tri_face.point_orientation(point) == OrientationTestResult::Positive { +// point_inside = false; +// } +// +// min_dist = T::min(min_dist, tri_face.distance(point)); +// } +// +// let sign = if point_inside { -1.0 } else { 1.0 }; +// +// SignedDistanceResult { +// feature_id: 0, +// point: Point {}, +// signed_distance: () +// } +// } +//} + +impl<'a, T> ConvexPolyhedron<'a, T> for Tetrahedron +where + T: RealField, +{ + type Face = Triangle3d; + + fn num_faces(&self) -> usize { + 4 + } + + fn get_face(&self, index: usize) -> Option { + let v = &self.vertices; + let tri = |i, j, k| Some(Triangle([v[i], v[j], v[k]])); + + // Must choose faces carefully so that they point towards the interior + match index { + 0 => tri(0, 1, 2), + 1 => tri(0, 3, 1), + 2 => tri(1, 3, 2), + 3 => tri(0, 2, 3), + _ => None, + } + } +} + +impl Distance> for Tetrahedron +where + T: RealField, +{ + fn distance(&self, point: &Point) -> T { + let triangle = |i, j, k| Triangle([self.vertices[i], self.vertices[j], self.vertices[k]]); + + let tri_faces = [ + // We must carefully choose the ordering of vertices so that the + // resulting faces have outwards-pointing normals + triangle(2, 1, 0), + triangle(1, 2, 3), + triangle(0, 1, 3), + triangle(2, 0, 3), + ]; + + let mut point_inside = true; + let mut min_dist = T::max_value(); + + for tri_face in &tri_faces { + // Remember that the triangles are oriented such that *outwards* is the positive + // direction, so for the point to be inside of the cell, we need its orientation + // with respect to each face to be *negative* + if tri_face.point_orientation(point) == OrientationTestResult::Positive { + point_inside = false; + } + + min_dist = T::min(min_dist, tri_face.distance(point)); + } + + if point_inside { + T::zero() + } else { + min_dist + } + } +} + +impl From> for ConvexPolygon +where + T: Scalar, +{ + fn from(triangle: Triangle2d) -> Self { + // TODO: Use Point2 in Mesh + ConvexPolygon::from_vertices(triangle.0.iter().map(|v| Point2::from(v.clone())).collect()) + } +} + +impl BoundedGeometry for Quad2d +where + T: RealField, +{ + type Dimension = U2; + + fn bounding_box(&self) -> AxisAlignedBoundingBox2d { + AxisAlignedBoundingBox2d::from_points(&self.0).expect("Triangle always has > 0 vertices") + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// A quadrilateral consisting of four vertices, assumed to be specified in counter-clockwise +/// winding order. +pub struct Quad2d(pub [Point2; 4]); + +impl Quad2d +where + T: RealField, +{ + /// Returns the index of a concave corner of the quadrilateral, if there is any. + pub fn concave_corner(&self) -> Option { + for i in 0..4 { + let x_next = self.0[(i + 2) % 4]; + let x_curr = self.0[(i + 1) % 4]; + let x_prev = self.0[(i + 1) % 4]; + + let a = x_next - x_curr; + let b = x_prev - x_curr; + // perp gives "2d cross product", which when negative means that the interior angle + // is creater than 180 degrees, and so the corner must be concave + if a.perp(&b) < T::zero() { + return Some(i + 1); + } + } + + None + } + + /// Splits the quad into two triangles represented by local indices { 0, 1, 2, 3 } + /// which correspond to the quad's vertices. + /// + /// While the quad may be concave, it is assumed that it has no self-intersections and that + /// all vertices are unique. + pub fn split_into_triangle_connectivities(&self) -> ([usize; 3], [usize; 3]) { + if let Some(concave_corner_index) = self.concave_corner() { + let i = concave_corner_index; + let triangle1 = [(i + 2) % 4, (i + 3) % 4, (i + 0) % 4]; + let triangle2 = [(i + 2) % 4, (i + 0) % 4, (i + 1) % 4]; + (triangle1, triangle2) + } else { + // Split arbitrarily, but in a regular fashion + let triangle1 = [0, 1, 2]; + let triangle2 = [0, 2, 3]; + (triangle1, triangle2) + } + } + + pub fn split_into_triangles(&self) -> (Triangle2d, Triangle2d) { + let (conn1, conn2) = self.split_into_triangle_connectivities(); + let mut vertices1 = [Point2::origin(); 3]; + let mut vertices2 = [Point2::origin(); 3]; + + for (v, idx) in izip!(&mut vertices1, &conn1) { + *v = self.0[*idx]; + } + + for (v, idx) in izip!(&mut vertices2, &conn2) { + *v = self.0[*idx]; + } + + let tri1 = Triangle(vertices1); + let tri2 = Triangle(vertices2); + + (tri1, tri2) + } + + pub fn area(&self) -> T { + let (tri1, tri2) = self.split_into_triangles(); + tri1.area() + tri2.area() + } +} + +impl Distance> for Quad2d +where + T: RealField, +{ + fn distance(&self, point: &Point2) -> T { + // TODO: Avoid heap allocation + GeneralPolygon::from_vertices(self.0.to_vec()).distance(point) + } +} + +impl TryFrom> for ConvexPolygon +where + T: RealField, +{ + type Error = ConcavePolygonError; + + fn try_from(value: Quad2d) -> Result { + if value.concave_corner().is_none() { + // TODO: Change Quad2d to have Point2 members instead of Vector2 + Ok(ConvexPolygon::from_vertices( + value.0.iter().map(|v| Point2::from(v.clone())).collect(), + )) + } else { + Err(ConcavePolygonError) + } + } +} diff --git a/fenris/src/geometry/polygon.rs b/fenris/src/geometry/polygon.rs new file mode 100644 index 0000000..11240c4 --- /dev/null +++ b/fenris/src/geometry/polygon.rs @@ -0,0 +1,306 @@ +use crate::geometry::{AxisAlignedBoundingBox2d, BoundedGeometry, Distance, LineSegment2d, Orientation}; +use itertools::{izip, Itertools}; +use nalgebra::{Point2, RealField, Scalar, Vector2, U2}; +use serde::{Deserialize, Serialize}; +use std::iter::once; + +use numeric_literals::replace_float_literals; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct GeneralPolygon +where + T: Scalar, +{ + vertices: Vec>, + // TODO: Also use acceleration structure for fast queries? +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ClosestEdge +where + T: Scalar, +{ + pub signed_distance: T, + pub edge_parameter: T, + pub edge_point: Point2, + pub edge_index: usize, +} + +pub trait Polygon +where + T: RealField, +{ + fn vertices(&self) -> &[Point2]; + + fn num_edges(&self) -> usize; + + fn get_edge(&self, index: usize) -> Option>; + + fn num_vertices(&self) -> usize { + self.vertices().len() + } + + /// Returns the given pseudonormal (angle-weighted normal) given an edge index and a parameter + /// representing a point on edge. + /// + /// If t == 0, then the average normal of this edge and its predecessor neighbor is returned. + /// If t == 1, then the average normal of this edge and its successor neighbor is returned. + /// Otherwise the normal of the edge is returned. + fn pseudonormal_on_edge(&self, edge_index: usize, t: T) -> Option>; + + fn for_each_edge(&self, mut func: impl FnMut(usize, LineSegment2d)) { + for edge_idx in 0..self.num_edges() { + let segment = self + .get_edge(edge_idx) + .expect("Edge index must exist, given that we're in the loop body."); + func(edge_idx, segment); + } + } + + fn closest_edge(&self, x: &Point2) -> Option> { + let mut closest_edge_index = None; + let mut smallest_squared_dist = T::max_value(); + + self.for_each_edge(|edge_idx, edge| { + let closest_point_on_edge = edge.closest_point(x); + let dist2 = (x - closest_point_on_edge).magnitude_squared(); + if dist2 < smallest_squared_dist { + closest_edge_index = Some(edge_idx); + smallest_squared_dist = dist2; + } + }); + + let closest_edge_index = closest_edge_index?; + // We unwrap all the results below, because since we have a concrete index, + // all results *must exist*, otherwise it's an error + let closest_edge = self.get_edge(closest_edge_index).unwrap(); + let t = closest_edge.closest_point_parametric(x); + let pseudonormal = self.pseudonormal_on_edge(closest_edge_index, t).unwrap(); + let closest_point_on_edge = closest_edge.point_from_parameter(t); + let d = x - &closest_point_on_edge; + let distance = d.magnitude(); + let sign = d.dot(&pseudonormal).signum(); + + Some(ClosestEdge { + signed_distance: sign * distance, + edge_parameter: t, + edge_point: closest_point_on_edge, + edge_index: closest_edge_index, + }) + } + + fn intersects_segment(&self, segment: &LineSegment2d) -> bool { + // A segment either + // - Intersects an edge in the polygon + // - Is completely contained in the polygon + // - Does not intersect the polygon at all + // To determine if it is completely contained in the polygon, we keep track of + // the closest edge to each endpoint of the segment. Technically, only one would be + // sufficient, but due to floating-point errors there are some cases where a segment may + // be classified as not intersecting an edge, yet only one of its endpoints will have + // a negative signed distance to the polygon. Thus, for robustness, we compute the signed + // distance of both endpoints. + if self.num_edges() == 0 { + return false; + } + + let mut closest_edges = [0, 0]; + let mut smallest_squared_dists = [T::max_value(), T::max_value()]; + let endpoints = [*segment.from(), *segment.to()]; + + let mut intersects = false; + + self.for_each_edge(|edge_idx, edge| { + if edge.intersect_segment_parametric(segment).is_some() { + intersects = true; + } else { + for (endpoint, closest_edge, smallest_dist2) in + izip!(&endpoints, &mut closest_edges, &mut smallest_squared_dists) + { + let closest_point_on_edge = edge.closest_point(endpoint); + let dist2 = (endpoint - closest_point_on_edge).magnitude_squared(); + if dist2 < *smallest_dist2 { + *closest_edge = edge_idx; + *smallest_dist2 = dist2; + } + } + } + }); + + for (endpoint, closest_edge_idx) in izip!(&endpoints, &closest_edges) { + // We can unwrap here, because we know that the Polygon has at least one edge + let closest_edge = self.get_edge(*closest_edge_idx).unwrap(); + let t = closest_edge.closest_point_parametric(endpoint); + let pseudonormal = self.pseudonormal_on_edge(*closest_edge_idx, t).unwrap(); + let closest_point_on_edge = closest_edge.point_from_parameter(t); + let sign = (endpoint - closest_point_on_edge).dot(&pseudonormal); + + if sign <= T::zero() { + return true; + } + } + + false + } + + /// Computes the signed area of the (simple) polygon. + /// + /// The signed area of a simple polygon is positive if the polygon has a counter-clockwise + /// orientation, or negative if it is oriented clockwise. + #[replace_float_literals(T::from_f64(literal).unwrap())] + fn signed_area(&self) -> T { + // The formula for the signed area of a simple polygon can easily be obtained from + // Green's formula by rewriting the surface integral that defines its area + // as an integral over the curve defining the polygon's boundary, + // which furthermore can be decomposed into a sum of integrals over each edge. + // See e.g. + // https://math.blogoverflow.com/2014/06/04/greens-theorem-and-area-of-polygons/ + // for details. + let two_times_signed_area = (0..self.num_edges()) + .map(|edge_idx| self.get_edge(edge_idx).unwrap()) + .map(|segment| { + let a = segment.from(); + let b = segment.to(); + (b.y - a.y) * (b.x + a.x) + }) + .fold(T::zero(), |sum, contrib| sum + contrib); + two_times_signed_area / 2.0 + } + + /// Computes the area of the (simple) polygon. + fn area(&self) -> T { + self.signed_area().abs() + } + + fn orientation(&self) -> Orientation { + let signed_area = self.signed_area(); + if signed_area > T::zero() { + Orientation::Counterclockwise + } else { + Orientation::Clockwise + } + } +} + +impl GeneralPolygon +where + T: Scalar, +{ + pub fn from_vertices(vertices: Vec>) -> Self { + Self { vertices } + } + + pub fn vertices(&self) -> &[Point2] { + &self.vertices + } + + pub fn transform_vertices(&mut self, mut transform: F) + where + F: FnMut(&mut [Point2]), + { + transform(&mut self.vertices) + + // TODO: Update acceleration structure etc., if we decide to internally use one later on + } + + pub fn num_vertices(&self) -> usize { + self.vertices.len() + } + + pub fn num_edges(&self) -> usize { + self.vertices.len() + } + + /// An iterator over edges as line segments + pub fn edge_iter<'a>(&'a self) -> impl 'a + Iterator> { + self.vertices + .iter() + .chain(once(self.vertices.first().unwrap())) + .tuple_windows() + .map(|(a, b)| LineSegment2d::new(a.clone(), b.clone())) + } +} + +impl GeneralPolygon +where + T: RealField, +{ + /// Corrects the orientation of the polygon. + /// + /// The first vertex is guaranteed to be the same before and after the orientation + /// change. + pub fn orient(&mut self, desired_orientation: Orientation) { + if desired_orientation != self.orientation() { + self.vertices.reverse(); + self.vertices.rotate_right(1); + } + } +} + +impl Polygon for GeneralPolygon +where + T: RealField, +{ + fn vertices(&self) -> &[Point2] { + &self.vertices + } + + fn num_edges(&self) -> usize { + self.vertices.len() + } + + fn get_edge(&self, index: usize) -> Option> { + let a = self.vertices.get(index)?; + let b = self.vertices.get((index + 1) % self.num_vertices())?; + Some(LineSegment2d::new(*a, *b)) + } + + #[replace_float_literals(T::from_f64(literal).unwrap())] + fn pseudonormal_on_edge(&self, edge_index: usize, t: T) -> Option> { + let edge = self.get_edge(edge_index)?; + let edge_normal = edge.normal_dir().normalize(); + + // TODO: Handle potentially degenerate line segments (i.e. they degenerate to a single + // point, and so normalization of the normal is arbitrary, if it at all works) + + let pseudonormal = if t == T::zero() { + // Have to take care not to underflow usize, so we cannot subtract directly + let previous_idx = ((edge_index + self.num_edges()) - 1) % self.num_edges(); + let previous_edge = self.get_edge(previous_idx)?; + let previous_edge_normal = previous_edge.normal_dir().normalize(); + ((previous_edge_normal + edge_normal) / 2.0).normalize() + } else if t == T::one() { + let next_idx = (edge_index + 1) % self.num_edges(); + let next_edge = self.get_edge(next_idx)?; + let next_edge_normal = next_edge.normal_dir().normalize(); + ((next_edge_normal + edge_normal) / 2.0).normalize() + } else { + edge_normal + }; + + Some(pseudonormal) + } +} + +impl BoundedGeometry for GeneralPolygon +where + T: RealField, +{ + type Dimension = U2; + + fn bounding_box(&self) -> AxisAlignedBoundingBox2d { + AxisAlignedBoundingBox2d::from_points(self.vertices()).expect("Vertex collection must be non-empty") + } +} + +impl Distance> for GeneralPolygon +where + T: RealField, +{ + fn distance(&self, point: &Point2) -> T { + let closest_edge = self + .closest_edge(point) + .expect("We don't support empty polygons at the moment (do we want to?)"); + T::max(closest_edge.signed_distance, T::zero()) + } +} diff --git a/fenris/src/geometry/polymesh.rs b/fenris/src/geometry/polymesh.rs new file mode 100644 index 0000000..10b3b2f --- /dev/null +++ b/fenris/src/geometry/polymesh.rs @@ -0,0 +1,901 @@ +use std::cmp::{max, min}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::error::Error; +use std::fmt; +use std::fmt::Display; +use std::hash::Hash; + +use itertools::Itertools; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, DimName, Point, Point3, RealField, Scalar, Vector3, U2, U3}; +use numeric_literals::replace_float_literals; +use serde::{Deserialize, Serialize}; +use std::fmt::Formatter; + +use crate::connectivity::Connectivity; +use crate::geometry::{ + compute_polyhedron_volume_from_faces, ConvexPolygon3d, ConvexPolyhedron, HalfSpace, LineSegment3d, +}; +use crate::mesh::Mesh; +use nested_vec::NestedVec; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +struct UndirectedEdge { + // Indices are always sorted, so that a <= b, for [a, b] + indices: [usize; 2], +} + +impl UndirectedEdge { + pub fn new(a: usize, b: usize) -> Self { + Self { + indices: [min(a, b), max(a, b)], + } + } + + pub fn indices(&self) -> &[usize; 2] { + &self.indices + } +} + +#[derive(Debug)] +pub struct PolyMeshFace<'a, T: Scalar, D: DimName> +where + DefaultAllocator: Allocator, +{ + all_vertices: &'a [Point], + face_vertex_indices: &'a [usize], +} + +impl<'a, T: Scalar> ConvexPolygon3d<'a, T> for PolyMeshFace<'a, T, U3> { + fn num_vertices(&self) -> usize { + self.face_vertex_indices.len() + } + + fn get_vertex(&self, index: usize) -> Option> { + let v = self + .all_vertices + .get(*self.face_vertex_indices.get(index)?) + .expect("Internal error: Vertex must always exist if the local index is valid."); + Some(v.clone()) + } +} + +/// A volumetric polytopal mesh. +/// +/// It is assumed that each polytopal cell is convex. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PolyMesh +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, +{ + #[serde(bound(serialize = ">::Buffer: Serialize"))] + #[serde(bound(deserialize = ">::Buffer: Deserialize<'de>"))] + vertices: Vec>, + faces: NestedVec, + cells: NestedVec, +} + +pub type PolyMesh2d = PolyMesh; +pub type PolyMesh3d = PolyMesh; + +impl PolyMesh +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, +{ + pub fn from_poly_data(vertices: Vec>, faces: NestedVec, cells: NestedVec) -> Self { + let num_vertices = vertices.len(); + let num_faces = faces.len(); + + if faces.iter_array_elements().any(|idx| *idx >= num_vertices) { + panic!("Vertex index out of bounds in faces description.") + } + + if cells.iter_array_elements().any(|idx| *idx >= num_faces) { + panic!("Face index out of bounds in cells description.") + } + + Self { + vertices, + faces: faces, + cells: cells, + } + } + + /// Creates a poly mesh by joining the face connectivity of each cell to a polygon (only works if the cells are topologically 2D) + pub fn from_surface_mesh(mesh: &Mesh) -> Self + where + C: Connectivity, + { + // TODO: Implement using the From trait? + + let mut old_to_new_vertex_indices: HashMap = HashMap::with_capacity(mesh.vertices().len()); + let mut faces = NestedVec::new(); + + // Convert cells to polygonal faces by extracting the cells face connectivity + for cell in mesh.connectivity() { + let num_faces = cell.num_faces(); + let mut polygon = Vec::new(); + for i in 0..num_faces { + let face_connectivity = cell.get_face_connectivity(i).unwrap(); + let new_vertices: Vec<_> = face_connectivity + .vertex_indices() + .iter() + .copied() + .map(|v_old| { + let v_new = old_to_new_vertex_indices.len(); + *old_to_new_vertex_indices.entry(v_old).or_insert(v_new) + }) + .collect(); + polygon.extend(new_vertices); + } + // Remove the last vertex if it is the same as the first + if let (Some(first), Some(last)) = (polygon.first(), polygon.last()) { + if *first == *last { + polygon.pop(); + } + } + // Remove repeating vertices (because of the concatenation of faces) + polygon.dedup(); + + faces.push(polygon.as_slice()); + } + + // Reorder the old vertex indices into the order used by the extracted faces + let old_vertex_indices = { + let mut old_to_new_vertex_indices: Vec<(_, _)> = old_to_new_vertex_indices.into_iter().collect(); + old_to_new_vertex_indices.sort_unstable_by(|(_, v_new_a), (_, v_new_b)| v_new_a.cmp(v_new_b)); + let old_vertex_indices: Vec<_> = old_to_new_vertex_indices + .into_iter() + .map(|(v_old, _)| v_old) + .collect(); + old_vertex_indices + }; + + // Extract the subset of vertices required by the faces + let mut vertices = Vec::with_capacity(old_vertex_indices.len()); + for v_old in old_vertex_indices { + vertices.push( + mesh.vertices() + .get(v_old) + .expect("missing vertex of cell face") + .clone(), + ) + } + + Self { + vertices, + faces, + cells: NestedVec::new(), + } + } + + pub fn vertices(&self) -> &[Point] { + &self.vertices + } + + pub fn vertices_mut(&mut self) -> &mut [Point] { + &mut self.vertices + } + + pub fn num_faces(&self) -> usize { + self.faces.len() + } + + pub fn num_cells(&self) -> usize { + self.cells.len() + } + + pub fn face_vertices<'a>(&'a self, face_idx: usize) -> impl 'a + Iterator> { + self.get_face_connectivity(face_idx) + .into_iter() + .flatten() + .map(move |vertex_idx| &self.vertices()[*vertex_idx]) + } + + pub fn face_connectivity_iter<'a>(&'a self) -> impl 'a + Iterator { + self.faces.iter() + } + + pub fn cell_connectivity_iter<'a>(&'a self) -> impl 'a + Iterator { + self.cells.iter() + } + + pub fn get_face_connectivity(&self, index: usize) -> Option<&[usize]> { + self.faces.get(index) + } + + pub fn get_cell_connectivity(&self, index: usize) -> Option<&[usize]> { + self.cells.get(index) + } + + pub fn get_face(&self, index: usize) -> Option> { + self.get_face_connectivity(index) + .map(|face_vertex_indices| PolyMeshFace { + all_vertices: &self.vertices, + face_vertex_indices, + }) + } + + /// Returns a nested array, in which each array i contains the indices of the cells + /// associated with face i. + pub fn compute_face_cell_connectivity(&self) -> NestedVec { + // TODO: Implement this more efficiently so that we don't construct a + // Vec>. Ideally we'd implement facilities for CompactArrayStorage to be able + // to push directly to existing arrays (by moving the array around in storage if necessary). + let mut connectivity = vec![Vec::new(); self.num_faces()]; + + for (cell_idx, cell) in self.cell_connectivity_iter().enumerate() { + for face_idx in cell { + connectivity[*face_idx].push(cell_idx); + } + } + + let mut compact = NestedVec::new(); + for face_cell_conn in connectivity { + compact.push(&face_cell_conn); + } + + compact + } + + /// Removes duplicate instances of topologically equivalent faces. + /// + /// Two faces are topologically equivalent if the sets of vertices connected by each face + /// are equivalent. + pub fn dedup_faces(&mut self) { + let mut sorted_face_connectivity = NestedVec::new(); + for face in self.face_connectivity_iter() { + sorted_face_connectivity.push(face); + sorted_face_connectivity.last_mut().unwrap().sort_unstable(); + } + + let mut new_faces = NestedVec::new(); + let mut connectivity_map = HashMap::new(); + // Store new_indices of faces + let mut face_relabel = Vec::with_capacity(self.num_faces()); + for i in 0..self.num_faces() { + let sorted_face_conn = sorted_face_connectivity.get(i).unwrap(); + // TODO: Rewrite using entry API to avoid double lookup + if let Some(new_face_index) = connectivity_map.get(sorted_face_conn) { + // We've already encountered a face with equivalent topology, + // so map this face to the one we've found already + face_relabel.push(*new_face_index); + } else { + // We haven't so far encountered a face with equivalent topology, + // so we add this face to the new collection of faces. + let new_index = new_faces.len(); + connectivity_map.insert(sorted_face_conn, new_index); + new_faces.push(self.get_face_connectivity(i).unwrap()); + face_relabel.push(new_index); + } + } + + self.faces = new_faces; + + for i in 0..self.num_cells() { + let cell = self.cells.get_mut(i).unwrap(); + + for face_idx in cell { + *face_idx = face_relabel[*face_idx]; + } + } + } + + /// Returns the indices of the faces which are only referenced by at most one cells. + pub fn find_boundary_faces(&self) -> Vec { + let mut face_occurences = vec![0; self.num_faces()]; + + for cell_faces in self.cell_connectivity_iter() { + for face in cell_faces { + face_occurences[*face] += 1; + } + } + + face_occurences + .into_iter() + .enumerate() + .filter_map(|(face_idx, count)| if count <= 1 { Some(face_idx) } else { None }) + .collect() + } + + /// Merges multiple meshes into a single instance of `PolyMesh`. + /// + /// The mesh vertices, faces and cells are simply relabeled and glued together so that they + /// form a well-defined PolyMesh. No mesh processing is performed. + pub fn concatenate<'a>(meshes: impl IntoIterator>) -> Self { + let meshes = meshes.into_iter(); + let mut vertices = Vec::new(); + let mut faces = NestedVec::new(); + let mut cells = NestedVec::new(); + + let mut vertex_offset = 0; + let mut face_offset = 0; + + for mesh in meshes { + vertices.extend(mesh.vertices().iter().cloned()); + + for face_vertices in mesh.face_connectivity_iter() { + let mut new_face_vertices = faces.begin_array(); + for vertex_idx in face_vertices { + new_face_vertices.push_single(vertex_idx + vertex_offset); + } + } + + for cell_faces in mesh.cell_connectivity_iter() { + let mut new_cell_faces = cells.begin_array(); + for face_idx in cell_faces { + new_cell_faces.push_single(face_idx + face_offset); + } + } + + vertex_offset += mesh.vertices().len(); + face_offset += mesh.num_faces(); + } + + Self::from_poly_data(vertices, faces, cells) + } +} + +impl PolyMesh +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator, +{ + /// Recursively splits each edge in the mesh the specified number of times + pub fn split_edges_n_times(&mut self, n_times: usize) { + for _ in 0..n_times { + self.split_edges() + } + } + + /// Splits the edges of all faces in the mesh by inserting a vertex at the midpoint of each edge + #[replace_float_literals(T::from_f64(literal).unwrap())] + pub fn split_edges(&mut self) { + let vertex_offset = self.vertices.len(); + let mut additional_vertices = Vec::new(); + let mut new_faces = NestedVec::new(); + let mut subdivided_edges = HashMap::new(); + + for face in self.faces.iter() { + let edges = face + .iter() + .copied() + .cycle() + .take(face.len() + 1) + .tuple_windows(); + let mut new_face = new_faces.begin_array(); + for (v1, v2) in edges { + let edge = [v1.min(v2), v1.max(v2)]; + let v12 = *subdivided_edges.entry(edge).or_insert_with(|| { + let new_vertex_index = vertex_offset + additional_vertices.len(); + let midpoint = Point::from((&self.vertices[v1].coords + &self.vertices[v2].coords) * 0.5); + additional_vertices.push(midpoint); + new_vertex_index + }); + new_face.push_single(v1); + new_face.push_single(v12); + } + } + + self.vertices.extend(additional_vertices); + self.faces = new_faces; + } +} + +impl PolyMesh3d +where + T: Scalar, +{ + /// Triangulate the polyhedral mesh. + /// + /// Note that the algorithm currently only gives non-degenerate results when each cell + /// is *strictly* convex, in the sense that no two faces of a cell are co-planar. + /// + /// TODO: Can we relax the strict convexity restriction without introducing additional + /// Steiner points into the triangulation? The restriction is explained in the + /// paper by Max (2000) (see comments in implementation). + pub fn triangulate(&self) -> Result, Box> { + // The implementation here follows the procedure described in + // Nelson Max (2000), "Consistent Subdivision of Convex Polyhedra into Tetrahedra" + // The key is as follows: Whenever subdividing a face/cell into triangles/tetrahedra, + // we do so by connecting the vertex of smallest index to the triangle faces + // that were obtained from faces not incident to the vertex. + + // First triangulate each face and maintain a map from the original face to the + // new face indices + + let mut triangulated_faces = NestedVec::new(); + let mut face_map = NestedVec::new(); + + for face in self.face_connectivity_iter() { + if face.len() < 3 { + return Err(Box::from( + "Encountered face with less than 3 vertices,\ + cannot triangulate.", + )); + } + + let mut face_map_array = face_map.begin_array(); + // Pick the vertex with smallest index + let (min_idx, _) = face + .iter() + .enumerate() + .min_by_key(|(_, v_idx)| *v_idx) + .unwrap(); + + for i in 0..face.len() - 2 { + let a = face[min_idx]; + let b = face[(i + 1 + min_idx) % face.len()]; + let c = face[(i + 2 + min_idx) % face.len()]; + face_map_array.push_single(triangulated_faces.len()); + triangulated_faces.push(&[a, b, c]); + } + } + + let mut tetrahedral_cells = NestedVec::new(); + for cell in self.cell_connectivity_iter() { + // Ignore empty cells + if cell.len() > 0 { + // Pick the vertex of least index in the cell + let v_idx = cell + .iter() + .flat_map(|face_idx| { + self.get_face_connectivity(*face_idx).expect( + "Logic error: Cell references face that \ + does not exist.", + ) + }) + .min() + .expect("Cell is non-empty"); + + // For each original face in the cell that does *not* contain the vertex, + // create new tetrahedral cells by connecting the chosen vertex + // to the triangulated faces. + // It is important to discard the *original* faces with this test, because + // otherwise we would create degenerate tetrahedral cells, as our + // chosen vertex would be in the same plane as the 3 vertices of the triangulated + // face. + for original_face_idx in cell { + let original_face_vertices = self + .get_face_connectivity(*original_face_idx) + .expect("Logic error: Cell references face that does not exist."); + + if !original_face_vertices.contains(v_idx) { + let triangulated_face_indices = face_map + .get(*original_face_idx) + .expect("Logic error: Cell references face that does not exist."); + + for tri_face_idx in triangulated_face_indices { + let tri_face_vertices = triangulated_faces.get(*tri_face_idx).unwrap(); + assert_eq!(tri_face_vertices.len(), 3); + + // Connect v to face by constructing 3 new triangular faces + let a = tri_face_vertices[0]; + let b = tri_face_vertices[1]; + let c = tri_face_vertices[2]; + let v = *v_idx; + + // Triangular faces denoted by vertices connected, + // i.e. abc means triangular face constructed by vertices a, b and c. + let abc_idx = *tri_face_idx; + let abv_idx = triangulated_faces.len(); + let bcv_idx = abv_idx + 1; + let cav_idx = bcv_idx + 1; + + // This will cause duplicated faces, but we deduplicate them + // as a post-process. TODO: Can we directly construct + // faces without duplicating faces in a succinct way? + triangulated_faces.push(&[a, b, v]); + triangulated_faces.push(&[b, c, v]); + triangulated_faces.push(&[c, a, v]); + tetrahedral_cells.push(&[abc_idx, abv_idx, bcv_idx, cav_idx]); + } + } + } + } + } + + let mut new_poly_mesh = + PolyMesh::from_poly_data(self.vertices().to_vec(), triangulated_faces, tetrahedral_cells); + new_poly_mesh.dedup_faces(); + Ok(new_poly_mesh) + } + + pub fn keep_cells(&self, cell_indices: &[usize]) -> Self { + // Use BTreeSets so that the relative order of the indices are kept + let keep_faces: BTreeSet<_> = cell_indices + .iter() + .flat_map(|cell_idx| { + self.get_cell_connectivity(*cell_idx) + .expect("All cell indices must be in bounds") + }) + .copied() + .collect(); + + let keep_vertices: BTreeSet<_> = keep_faces + .iter() + .flat_map(|face_idx| self.get_face_connectivity(*face_idx).unwrap()) + .copied() + .collect(); + + let faces_old_to_new_map: HashMap<_, _> = keep_faces + .iter() + .enumerate() + .map(|(new_idx, old_idx)| (old_idx, new_idx)) + .collect(); + + let vertices_old_to_new_map: HashMap<_, _> = keep_vertices + .iter() + .enumerate() + .map(|(new_idx, old_idx)| (*old_idx, new_idx)) + .collect(); + + let new_vertices = keep_vertices + .iter() + .map(|old_vertex_idx| self.vertices()[*old_vertex_idx].clone()) + .collect(); + + let mut new_faces = NestedVec::new(); + for old_face_idx in &keep_faces { + let old_face_vertices = self.get_face_connectivity(*old_face_idx).unwrap(); + let mut new_face_vertices = new_faces.begin_array(); + + for old_vertex_idx in old_face_vertices { + let new_vertex_idx = vertices_old_to_new_map.get(old_vertex_idx).unwrap(); + new_face_vertices.push_single(*new_vertex_idx); + } + } + + let mut new_cells = NestedVec::new(); + for old_cell_idx in cell_indices { + let old_cell_faces = self.get_cell_connectivity(*old_cell_idx).unwrap(); + let mut new_cell_faces = new_cells.begin_array(); + + for old_face_idx in old_cell_faces { + let new_face_idx = faces_old_to_new_map.get(old_face_idx).unwrap(); + new_cell_faces.push_single(*new_face_idx); + } + } + + Self::from_poly_data(new_vertices, new_faces, new_cells) + } +} + +/// Marks vertices according to whether or not they are contained in the half space. +/// +/// More precisely, given N vertices, a vector of N boolean values is returned. +/// If element `i` is `true`, then vertex `i` is contained in the half space. +fn mark_vertices(vertices: &[Point3], half_space: &HalfSpace) -> Vec { + vertices + .iter() + .map(|v| half_space.contains_point(v)) + .collect() +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Classification { + Inside, + Outside, + Cut, +} + +//fn classify_face(face_vertices: &[usize], vertex_table: &[bool]) -> Classification { +// let num_outside_vertices = face_vertices.iter().map(|i| vertex_table[*i]).count(); +// +// if num_outside_vertices == 0 { +// Classification::Inside +// } else if num_outside_vertices == face_vertices.len() { +// Classification::Outside +// } else { +// Classification::Cut +// } +//} + +fn is_intersection_vertex(vertex_edge_representation: &UndirectedEdge) -> bool { + let [a, b] = vertex_edge_representation.indices(); + a != b +} + +impl PolyMesh3d +where + T: RealField, +{ + pub fn translate(&mut self, translation: &Vector3) { + for v in self.vertices_mut() { + *v += translation; + } + } + + pub fn translated(mut self, translation: &Vector3) -> Self { + self.translate(translation); + self + } + + #[replace_float_literals(T::from_f64(literal).unwrap())] + pub fn compute_volume(&self) -> T { + let boundary_faces = self.find_boundary_faces(); + let face_iter = boundary_faces + .iter() + .map(|face_idx| self.get_face(*face_idx).unwrap()); + + compute_polyhedron_volume_from_faces(face_iter) + } + + pub fn intersect_convex_polyhedron<'a>(&self, polyhedron: &impl ConvexPolyhedron<'a, T>) -> Self { + let mut mesh = self.clone(); + for i in 0..polyhedron.num_faces() { + let face = polyhedron.get_face(i).unwrap(); + if let Some(half_space) = face.compute_half_space() { + mesh = mesh.intersect_half_space(&half_space); + } + } + mesh + } + + pub fn intersect_half_space(&self, half_space: &HalfSpace) -> Self { + // Our approach will be to first build up the complete description of faces + // by representing vertices as undirected edges between two vertices a and b. + // The physical vertex is defined to be the closest point to the half space on the + // line segment connecting a and b. + // In particular, if a == b, then the vertex is a vertex in the original mesh. + // If a != b, then the vertex is an intersection point between an edge and the half space. + + // A vertex is a member of the half space if the half space contains the vertex. + let vertex_half_space_membership = mark_vertices(&self.vertices, half_space); + + let mut new_faces = NestedVec::new(); + let mut face_classifications = Vec::with_capacity(self.num_faces()); + + for face_vertices in self.faces.iter() { + let mut new_face_vertices = new_faces.begin_array(); + + let mut classification = Classification::Inside; + + let face_vertices = face_vertices.iter().chain(face_vertices.first()).copied(); + for (a, b) in face_vertices.tuple_windows() { + let a_is_inside = vertex_half_space_membership[a]; + let b_is_inside = vertex_half_space_membership[b]; + + if a_is_inside { + new_face_vertices.push_single(UndirectedEdge::new(a, a)); + } + + if a_is_inside != b_is_inside { + // Edge is cut + new_face_vertices.push_single(UndirectedEdge::new(a, b)); + classification = Classification::Cut; + } + } + + // Only vertices which are somehow inside of the half space get added. + // Thus, if we added no vertices, the face is entirely outside. + if new_face_vertices.count() == 0 { + classification = Classification::Outside; + } + + face_classifications.push(classification); + } + + #[derive(Debug)] + struct IntersectionEdge { + // face_idx: usize, + a: UndirectedEdge, + b: UndirectedEdge, + } + + let mut intersection_edges = Vec::new(); + + let mut new_cells = NestedVec::new(); + for cell_faces in self.cells.iter() { + intersection_edges.clear(); + let mut new_cell_faces = new_cells.begin_array(); + + for face_idx in cell_faces { + let face_classification = face_classifications[*face_idx]; + match face_classification { + Classification::Inside => { + new_cell_faces.push_single(*face_idx); + } + Classification::Outside => {} + Classification::Cut => { + new_cell_faces.push_single(*face_idx); + let face_vertices = new_faces + .get(*face_idx) + .expect("Invalid face index referenced in cell."); + let face_vertices = face_vertices.iter().chain(face_vertices.first()); + for (a, b) in face_vertices.tuple_windows() { + // We're looking for edges that connect two intersection vertices + // (i.e. new vertices that result from the intersection with the plane). + if is_intersection_vertex(a) && is_intersection_vertex(b) { + intersection_edges.push(IntersectionEdge { + // face_idx: *face_idx, + a: *a, + b: *b, + }); + } + } + } + } + } + + // At this point we know which edges that are involved in creating new faces. + // In order to connect them together, we pick a random one and then start + // stringing them together as long as this is possible. For each such + // separate sequence, we generate a new face. Under non-degenerate situations, + // only one such face should get created. + while let Some(start_edge) = intersection_edges.pop() { + // TODO: Are we guaranteed that we don't accidentally pick another intersection + // edge from a face we have already visited? I *think* that this is the case, + // but I am not sure. We *might* have to additionally keep track of + // faces that we have picked? + + let new_face_index = new_faces.len(); + let mut new_face_vertices = new_faces.begin_array(); + + new_face_vertices.push_single(start_edge.a); + let mut next_vertex = start_edge.b; + + while let Some(next_local_idx) = intersection_edges + .iter() + .position(|edge| edge.a == next_vertex || edge.b == next_vertex) + { + let next_edge = intersection_edges.swap_remove(next_local_idx); + if next_edge.a == next_vertex { + new_face_vertices.push_single(next_edge.a); + next_vertex = next_edge.b; + } else { + new_face_vertices.push_single(next_edge.b); + next_vertex = next_edge.a; + } + } + + new_cell_faces.push_single(new_face_index); + } + } + + // At this point, we have fully constructed all faces and cells, + // but vertices are all represented in the point-on-edge representation. + // Moreover, we need to remove unused vertices, empty faces (faces that were + // removed but kept as empty sets in order to retain stable indexing) and cells. + let vertex_label_map = generate_edge_repr_vertex_labels( + new_faces + .iter() + .flat_map(|face_vertices| face_vertices) + .copied(), + ); + + // Compute new vertex coordinates where necessary + let mut final_vertices = vec![Point3::origin(); vertex_label_map.len()]; + for (edge_rep, new_vertex_idx) in &vertex_label_map { + let [a, b] = *edge_rep.indices(); + let vertex_coords = if a == b { + self.vertices[a] + } else { + let v_a = self.vertices[a]; + let v_b = self.vertices[b]; + let segment = LineSegment3d::from_end_points([v_a, v_b]); + segment.closest_point_to_plane(&half_space.plane()) + }; + final_vertices[*new_vertex_idx] = vertex_coords; + } + + // Convert faces from edge representation to new indices, + // and remove and remap empty faces + let (final_faces, face_label_map) = relabel_face_edge_representations(&new_faces, &vertex_label_map); + + // TODO: If we're a little more clever earlier on, we wouldn't have to + // allocate a whole new storage here + let mut final_cells = NestedVec::new(); + for cell_faces in new_cells.iter() { + if !cell_faces.is_empty() { + let mut new_cell_faces = final_cells.begin_array(); + for cell_face in cell_faces { + new_cell_faces.push_single( + *face_label_map + .get(cell_face) + .expect("Logic error: Face index is not in map"), + ); + } + } + } + + PolyMesh3d::from_poly_data(final_vertices, final_faces, final_cells) + } +} + +fn generate_edge_repr_vertex_labels( + vertices_in_edge_repr: impl IntoIterator, +) -> BTreeMap { + let mut map = BTreeMap::new(); + let mut iter = vertices_in_edge_repr.into_iter(); + + let mut next_available_index = 0; + while let Some(vertex) = iter.next() { + map.entry(vertex).or_insert_with(|| { + let idx = next_available_index; + next_available_index += 1; + idx + }); + } + + map +} + +/// Computes the standard index representation given edge representations of a collection +/// of faces and a label mapping for vertices. +/// +/// Empty faces are removed as part of the process. A mapping is returned which +/// serves to map "old" face indices to new indices. +fn relabel_face_edge_representations( + faces: &NestedVec, + vertex_label_map: &BTreeMap, +) -> (NestedVec, HashMap) { + let mut new_faces = NestedVec::new(); + let mut face_label_map = HashMap::new(); + let mut next_available_index = 0; + { + for (i, face_vertices) in faces.iter().enumerate() { + if !face_vertices.is_empty() { + let mut new_face_vertices = new_faces.begin_array(); + for vertex_edge_rep in face_vertices { + new_face_vertices.push_single( + *vertex_label_map + .get(vertex_edge_rep) + .expect("Logic error: Label map must map all relevant vertices."), + ); + } + + face_label_map.insert(i, next_available_index); + next_available_index += 1; + } + } + } + + (new_faces, face_label_map) +} + +impl Display for PolyMesh3d +where + T: Scalar + Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + writeln!(f, "PolyMesh3d")?; + writeln!(f, " Vertices:")?; + for v in self.vertices() { + writeln!(f, " {}", v)?; + } + writeln!(f)?; + + writeln!(f, " Face vertex indices:")?; + for (i, vertex_indices) in self.face_connectivity_iter().enumerate() { + write!(f, " {}: ", i)?; + if let Some(first) = vertex_indices.first() { + write!(f, "{}", first)?; + } + for idx in vertex_indices.iter().skip(1) { + write!(f, ", {}", idx)?; + } + writeln!(f)?; + } + writeln!(f)?; + + writeln!(f, " Cell face indices:")?; + for (i, face_indices) in self.cell_connectivity_iter().enumerate() { + write!(f, " {}: ", i)?; + if let Some(first) = face_indices.first() { + write!(f, "{}", first)?; + } + for idx in face_indices.iter().skip(1) { + write!(f, ", {}", idx)?; + } + writeln!(f)?; + } + + Ok(()) + } +} diff --git a/fenris/src/geometry/polytope.rs b/fenris/src/geometry/polytope.rs new file mode 100644 index 0000000..fdf1504 --- /dev/null +++ b/fenris/src/geometry/polytope.rs @@ -0,0 +1,487 @@ +use crate::geometry::{GeneralPolygon, Triangle, Triangle2d}; +use itertools::Itertools; +use nalgebra::{clamp, Matrix2, Point2, RealField, Scalar, Unit, Vector2}; + +/// Type used to indicate conversion failure in the presence of concavity. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct ConcavePolygonError; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConvexPolygon +where + T: Scalar, +{ + // TODO: SmallVec? + // Edges are implicitly represented as (i, i + 1) + vertices: Vec>, +} + +#[derive(Debug, Clone)] +pub struct HalfPlane +where + T: Scalar, +{ + point: Point2, + normal: Unit>, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct LineSegment2d +where + T: Scalar, +{ + from: Point2, + to: Point2, +} + +impl LineSegment2d +where + T: Scalar, +{ + pub fn new(from: Point2, to: Point2) -> Self { + Self { from, to } + } + + pub fn from(&self) -> &Point2 { + &self.from + } + + pub fn to(&self) -> &Point2 { + &self.to + } + + pub fn reverse(&self) -> Self { + LineSegment2d { + from: self.to.clone(), + to: self.from.clone(), + } + } +} + +impl LineSegment2d +where + T: RealField, +{ + pub fn to_line(&self) -> Line2d { + let dir = &self.to - &self.from; + Line2d::from_point_and_dir(self.from.clone(), dir) + } + + /// Returns a vector tangent to the line segment. + /// + /// Note that the vector is **not** normalized. + pub fn tangent_dir(&self) -> Vector2 { + self.to().coords - self.from().coords + } + + /// Returns a vector normal to the line segment, in the direction consistent with a + /// counter-clockwise winding order when the edge is part of a polygon. + /// + /// Note that the vector is **not** normalized. + pub fn normal_dir(&self) -> Vector2 { + let tangent = self.tangent_dir(); + Vector2::new(tangent.y, -tangent.x) + } + + pub fn length(&self) -> T { + self.tangent_dir().norm() + } + + pub fn midpoint(&self) -> Point2 { + Point2::from((self.from.coords + self.to.coords) / (T::one() + T::one())) + } + + pub fn intersect_line_parametric(&self, line: &Line2d) -> Option { + self.to_line() + .intersect_line_parametric(line) + .map(|(t1, _)| t1) + } + + /// Compute the closest point on the segment to the given point, represented in + /// the parametric form x = a + t * (b - a). + pub fn closest_point_parametric(&self, point: &Point2) -> T { + let t = self.to_line().project_point_parametric(point); + clamp(t, T::zero(), T::one()) + } + + /// Computes the closest point on the line to the given point. + pub fn closest_point(&self, point: &Point2) -> Point2 { + let t = self.closest_point_parametric(point); + self.point_from_parameter(t) + } + + pub fn point_from_parameter(&self, t: T) -> Point2 { + Point2::from(self.from().coords + (self.to() - self.from()) * t) + } + + /// Computes the intersection of two line segments (if any), but returns the result as a parameter. + /// + /// Let all points on this line segment be defined by the relation x = a + t * (b - a) + /// for 0 <= t <= 1. Then, if the two line segments intersect, t is returned. Otherwise, + /// `None` is returned. + pub fn intersect_segment_parametric(&self, other: &LineSegment2d) -> Option { + // Represent the two lines as: + // x1 = a1 + t1 * d1 + // x2 = a2 + t2 * d2 + // where di = bi - ai. This gives the linear system + // [ d1 -d2 ] t = a2 - a1, + // where t = [t1, t2]. + + let d1 = &self.to - &self.from; + let d2 = &other.to - &other.from; + + let line1 = Line2d::from_point_and_dir(self.from.clone(), d1); + let line2 = Line2d::from_point_and_dir(other.from.clone(), d2); + + line1 + .intersect_line_parametric(&line2) + .and_then(|(t1, t2)| { + // TODO: This may go very wrong if we're talking "exact" intersection + // e.g. when a line segment intersects another segment only at a point, + // in which case we might discard the intersection entirely. + // I suppose the only way to deal with this is either arbitrary precision + // or using epsilons? Also, keep in mind that the `from` and `to` + // points may already be suffering from imprecision! + if t2 < T::zero() || t2 > T::one() { + None + } else if t1 < T::zero() || t1 > T::one() { + None + } else { + Some(t1) + } + }) + } + + pub fn intersect_polygon(&self, other: &ConvexPolygon) -> Option> { + let mut min = None; + let mut max = None; + + let contains_start = other.contains_point(self.from()); + let contains_end = other.contains_point(self.to()); + let contained_in_poly = contains_start && contains_end; + + if contains_start { + min = Some(T::zero()); + } + if contains_end { + max = Some(T::one()); + } + + if !contained_in_poly { + for edge in other.edges() { + let edge_segment = LineSegment2d::new(*edge.0, *edge.1); + + if let Some(t) = self.intersect_segment_parametric(&edge_segment) { + if t < *min.get_or_insert(t) { + min = Some(t); + } + + if t > *max.get_or_insert(t) { + max = Some(t) + } + } + } + } + + // TODO: I think this *can* actually occur if the polygon is e.g. a point + assert!(min.is_none() == max.is_none()); + + // Once we have t_min and t_max (or we don't and we return None), + // we construct the resulting line segment + min.and_then(|min| max.and_then(|max| Some((min, max)))) + .map(|(t_min, t_max)| { + let a = self.from(); + let b = self.to(); + let d = b - a; + debug_assert!(t_min <= t_max); + LineSegment2d::new(a + d * t_min, a + d * t_max) + }) + } +} + +impl From> for ConvexPolygon +where + T: Scalar, +{ + fn from(segment: LineSegment2d) -> Self { + ConvexPolygon::from_vertices(vec![segment.from, segment.to]) + } +} + +#[derive(Debug, Clone)] +pub struct Line2d +where + T: Scalar, +{ + point: Point2, + dir: Vector2, +} + +impl Line2d +where + T: Scalar, +{ + pub fn from_point_and_dir(point: Point2, dir: Vector2) -> Self { + // TODO: Make dir Unit? + Self { point, dir } + } +} + +impl Line2d +where + T: RealField, +{ + /// Computes the projection of the given point onto the line, representing the point + /// in parametric form. + pub fn project_point_parametric(&self, point: &Point2) -> T { + let d2 = self.dir.magnitude_squared(); + (point - &self.point).dot(&self.dir) / d2 + } + + /// Computes the projection of the given point onto the line. + pub fn project_point(&self, point: &Point2) -> Point2 { + let t = self.project_point_parametric(point); + self.point_from_parameter(t) + } + + pub fn point_from_parameter(&self, t: T) -> Point2 { + &self.point + &self.dir * t + } + + pub fn intersect(&self, other: &Line2d) -> Option> { + self.intersect_line_parametric(other) + .map(|(t1, _)| self.point_from_parameter(t1)) + } + + /// Computes the intersection of two lines, if any. + /// + /// Let all points on each line segment be defined by the relation `x1 = a1 + t1 * d1` + /// for `0 <= t1 <= 1`, and similarly for `t2`. Here, `t1` is the parameter associated with + /// `self`, and `t2` is the parameter associated with `other`. + pub fn intersect_line_parametric(&self, other: &Line2d) -> Option<(T, T)> { + // Represent the two lines as: + // x1 = a1 + t1 * d1 + // x2 = a2 + t2 * d2 + // where di = bi - ai. This gives the linear system + // [ d1 -d2 ] t = a2 - a1, + // where t = [t1, t2]. + + let rhs = &other.point - &self.point; + let matrix = Matrix2::from_columns(&[self.dir, -other.dir]); + + // TODO: Rewrite to use LU decomposition? + matrix + .try_inverse() + .map(|inv| inv * rhs) + // Inverse returns vector, split it up into its components + .map(|t| (t.x, t.y)) + } +} + +impl HalfPlane +where + T: RealField, +{ + pub fn from_point_and_normal(point: Point2, normal: Unit>) -> Self { + Self { point, normal } + } +} + +impl HalfPlane +where + T: RealField, +{ + pub fn contains_point(&self, point: &Point2) -> bool { + self.surface_distance_to_point(point) >= T::zero() + } + + pub fn surface_distance_to_point(&self, point: &Point2) -> T { + let d = point - &self.point; + self.normal.dot(&d) + } + + pub fn point(&self) -> &Point2 { + &self.point + } + + pub fn normal(&self) -> &Vector2 { + &self.normal + } + + /// Returns a line representing the surface of the half plane + pub fn surface(&self) -> Line2d { + let tangent = Vector2::new(self.normal.y, -self.normal.x); + Line2d::from_point_and_dir(self.point.clone(), tangent) + } +} + +impl ConvexPolygon +where + T: Scalar, +{ + /// Construct a new convex polygon from the given vertices, assumed to be ordered in a + /// counter-clockwise way such that (i, i + 1) forms an edge between vertex i and i + 1. + /// + /// It is assumed that the polygon is convex. + pub fn from_vertices(vertices: Vec>) -> ConvexPolygon { + Self { vertices } + } + + pub fn vertices(&self) -> &[Point2] { + &self.vertices + } + + /// Returns the number of edges in the polygon. Note that a single point has 1 edge, + /// pointing from itself to itself, a line segment has two edges, and in general + /// the number of edges is equal to the number of vertices. + pub fn num_edges(&self) -> usize { + self.vertices().len() + } + + pub fn edges(&self) -> impl Iterator, &Point2)> { + let num_vertices = self.vertices().len(); + self.vertices() + .iter() + .cycle() + .take(num_vertices + 1) + .tuple_windows() + } + + pub fn is_empty(&self) -> bool { + self.vertices.is_empty() + } + + pub fn is_point(&self) -> bool { + self.vertices.len() == 1 + } + + pub fn is_line_segment(&self) -> bool { + self.vertices.len() == 2 + } +} + +impl ConvexPolygon +where + T: RealField, +{ + /// Iterates over the half planes that define the polygon. + /// + /// Every non-degenerate polygon can be represented by the intersection of a finite number + /// of closed half-planes. + /// + /// If the polygon is degenerate, the intersection of the half planes returned by this method + /// will in general not be sufficient to describe the polygon. + pub fn half_planes<'a>(&'a self) -> impl Iterator> + 'a { + self.edges().filter_map(|(v1, v2)| { + if v1 != v2 { + let edge_dir = v2 - v1; + let negative_edge_normal = Vector2::new(-edge_dir.y, edge_dir.x); + let normalized_negative_edge_normal = Unit::try_new(negative_edge_normal, T::zero()) + .expect("v1 != v2, so vector can be safely normalized"); + Some(HalfPlane::from_point_and_normal(*v1, normalized_negative_edge_normal)) + } else { + None + } + }) + } + + /// Determines if the (closed) convex polygon contains the given point. + pub fn contains_point(&self, point: &Point2) -> bool { + if self.is_point() { + self.vertices.first().unwrap() == point + } else if self.is_line_segment() { + unimplemented!() + } else { + self.half_planes() + .all(|half_plane| half_plane.contains_point(point)) + } + } + + /// Computes the intersection with the current polygon and the given half plane, + /// and returns a new polygon that holds the result. + /// + /// Note: No steps have been made to make this routine numerically robust. + /// TODO: Make numerically robust? + pub fn intersect_halfplane(&self, half_plane: &HalfPlane) -> ConvexPolygon { + let mut new_vertices = Vec::new(); + + // Handle special case of the polygon consisting of a single vertex + if self.vertices.len() == 1 { + let first = self.vertices().first().unwrap(); + if half_plane.contains_point(first) { + new_vertices.push(first.clone()); + } + } else { + for (v1, v2) in self.edges() { + let v1_contained = half_plane.contains_point(v1); + let v2_contained = half_plane.contains_point(v2); + if v1_contained { + new_vertices.push(v1.clone()); + } + + if (v1_contained && !v2_contained) || (!v1_contained && v2_contained) { + // Edge is intersected, add vertex at intersection point + let dir = (v2 - v1).normalize(); + let intersection_point = half_plane + .surface() + .intersect(&Line2d::from_point_and_dir(v1.clone(), dir)) + .expect( + "We already know that the line must intersect the edge, \ + so this should work unless we have some ugly numerical \ + artifacts.", + ); + + new_vertices.push(intersection_point); + } + } + } + + ConvexPolygon::from_vertices(new_vertices) + } + + /// Computes the intersection of this polygon and the given convex polygon. + pub fn intersect_polygon(&self, other: &ConvexPolygon) -> Self { + // TODO: Deal with degeneracies + if self.is_point() || other.is_point() { + unimplemented!() + } else if self.is_line_segment() { + let segment = LineSegment2d::new(self.vertices[0], self.vertices[1]); + segment + .intersect_polygon(other) + .map(|segment| ConvexPolygon::from_vertices(vec![*segment.from(), *segment.to()])) + .unwrap_or_else(|| ConvexPolygon::from_vertices(Vec::new())) + } else if other.is_line_segment() { + other.intersect_polygon(self) + } else { + let mut result = self.clone(); + for half_plane in other.half_planes() { + result = result.intersect_halfplane(&half_plane); + } + result + } + } + + /// Splits the convex polygon into a set of disjoint triangles that exactly cover the area of the + /// polygon. + pub fn triangulate<'a>(&'a self) -> impl Iterator> + 'a { + self.edges() + // Use saturating subtraction so that we don't overflow and get an empty + // iterator in the case that the polygon has no vertices + .take(self.num_edges().saturating_sub(1)) + .skip(1) + .map(move |(a, b)| Triangle([*self.vertices.first().unwrap(), *a, *b])) + } + + pub fn triangulate_into_vec(&self) -> Vec> { + self.triangulate().collect() + } +} + +impl From> for GeneralPolygon +where + T: Scalar, +{ + fn from(poly: ConvexPolygon) -> Self { + GeneralPolygon::from_vertices(poly.vertices) + } +} diff --git a/fenris/src/geometry/procedural.rs b/fenris/src/geometry/procedural.rs new file mode 100644 index 0000000..749cd42 --- /dev/null +++ b/fenris/src/geometry/procedural.rs @@ -0,0 +1,283 @@ +use crate::connectivity::{Hex8Connectivity, Quad4d2Connectivity}; +use crate::geometry::polymesh::PolyMesh3d; +use crate::geometry::sdf::BoundedSdf; +use crate::geometry::{AxisAlignedBoundingBox2d, HalfSpace}; +use crate::mesh::{HexMesh, Mesh, QuadMesh2d, TriangleMesh2d}; +use nalgebra::{convert, try_convert, Point2, Point3, RealField, Unit, Vector2, Vector3}; +use numeric_literals::replace_float_literals; +use ordered_float::NotNan; +use std::cmp::min; +use std::f64::consts::PI; + +pub fn create_unit_square_uniform_quad_mesh_2d(cells_per_dim: usize) -> QuadMesh2d +where + T: RealField, +{ + create_rectangular_uniform_quad_mesh_2d(T::one(), 1, 1, cells_per_dim, &Vector2::new(T::zero(), T::one())) +} + +/// Generates an axis-aligned rectangular uniform mesh given a unit length, +/// dimensions as multipliers of the unit length and the number of cells per unit length. +pub fn create_rectangular_uniform_quad_mesh_2d( + unit_length: T, + units_x: usize, + units_y: usize, + cells_per_unit: usize, + top_left: &Vector2, +) -> QuadMesh2d +where + T: RealField, +{ + if cells_per_unit == 0 || units_x == 0 || units_y == 0 { + QuadMesh2d::from_vertices_and_connectivity(Vec::new(), Vec::new()) + } else { + let mut vertices = Vec::new(); + let mut cells = Vec::new(); + + let cell_size = T::from_f64(unit_length.to_subset().unwrap() / cells_per_unit as f64).unwrap(); + let num_cells_x = units_x * cells_per_unit; + let num_cells_y = units_y * cells_per_unit; + let num_vertices_x = num_cells_x + 1; + let num_vertices_y = num_cells_y + 1; + + let to_global_vertex_index = |i, j| (num_cells_x + 1) * j + i; + + for j in 0..num_vertices_y { + for i in 0..num_vertices_x { + let i_as_t = T::from_usize(i).expect("Must be able to fit usize in T"); + let j_as_t = T::from_usize(j).expect("Must be able to fit usize in T"); + let v = top_left + Vector2::new(i_as_t, -j_as_t) * cell_size; + vertices.push(Point2::from(v)); + } + } + + for j in 0..num_cells_y { + for i in 0..num_cells_x { + let quad = Quad4d2Connectivity([ + to_global_vertex_index(i, j + 1), + to_global_vertex_index(i + 1, j + 1), + to_global_vertex_index(i + 1, j), + to_global_vertex_index(i, j), + ]); + cells.push(quad); + } + } + + QuadMesh2d::from_vertices_and_connectivity(vertices, cells) + } +} + +#[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] +pub fn voxelize_bounding_box_2d(bounds: &AxisAlignedBoundingBox2d, max_cell_size: T) -> QuadMesh2d +where + T: RealField, +{ + assert!(max_cell_size > T::zero(), "Max cell size must be positive."); + + let extents = bounds.extents(); + let enlarged_bounds = AxisAlignedBoundingBox2d::new(bounds.min() - extents * 0.01, bounds.max() + extents * 0.01); + let enlarged_extents = enlarged_bounds.extents(); + + // Determine the minimum number of cells needed in each direction to completely cover + // the enlarged bounding box. We do this in double precision + let enlarged_extents_f64: Vector2 = try_convert(enlarged_extents).expect("Must be able to fit extents in f64"); + let resolution_f64: f64 = try_convert(max_cell_size).expect("Must be able to fit resolution in f64"); + + let candidate_num_cells_x = (enlarged_extents_f64.x / resolution_f64).ceil(); + let candidate_num_cells_y = (enlarged_extents_f64.y / resolution_f64).ceil(); + let candidate_cell_size_x = enlarged_extents_f64.x / candidate_num_cells_x; + let candidate_cell_size_y = enlarged_extents_f64.y / candidate_num_cells_y; + let cell_size_f64 = min( + NotNan::new(candidate_cell_size_x).unwrap(), + NotNan::new(candidate_cell_size_y).unwrap(), + ) + .into_inner(); + + let num_cells_x = (enlarged_extents_f64.x / cell_size_f64).ceil(); + let num_cells_y = (enlarged_extents_f64.y / cell_size_f64).ceil(); + let final_extents_x = num_cells_x * cell_size_f64; + let final_extents_y = num_cells_y * cell_size_f64; + let final_extents: Vector2 = Vector2::new(convert(final_extents_x), convert(final_extents_y)); + + let center = bounds.center(); + let top_left = Vector2::new(center.x - final_extents.x / 2.0, center.y + final_extents.y / 2.0); + + create_rectangular_uniform_quad_mesh_2d( + convert(cell_size_f64), + num_cells_x as usize, + num_cells_y as usize, + 1, + &top_left, + ) +} + +#[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] +pub fn voxelize_sdf_2d(sdf: &impl BoundedSdf, max_cell_size: T) -> QuadMesh2d +where + T: RealField, +{ + let rectangular_mesh: QuadMesh2d = voxelize_bounding_box_2d(&sdf.bounding_box(), max_cell_size); + let desired_cell_indices: Vec<_> = rectangular_mesh + .cell_iter() + .enumerate() + .filter(|(_, quad)| quad.0.iter().any(|v| sdf.eval(v) <= T::zero())) + .map(|(cell_index, _)| cell_index) + .collect(); + + rectangular_mesh.keep_cells(&desired_cell_indices) +} + +#[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] +pub fn approximate_quad_mesh_for_sdf_2d(sdf: &impl BoundedSdf, max_cell_size: T) -> QuadMesh2d +where + T: RealField, +{ + let mut mesh = voxelize_sdf_2d(sdf, max_cell_size); + + mesh.transform_vertices(|vertex| { + let phi = sdf.eval(vertex); + if phi > 0.0 { + let grad = sdf.gradient(vertex).expect("TODO: Fix when no gradient"); + let new_vertex = &*vertex - grad * phi; + *vertex = new_vertex; + } + }); + + mesh +} + +pub fn approximate_triangle_mesh_for_sdf_2d(sdf: &impl BoundedSdf, max_cell_size: T) -> TriangleMesh2d +where + T: RealField, +{ + // TODO: This is not the most efficient way to do this, since we compute SDFs for the same points + // several times, but it's at least simple + let mut mesh = voxelize_sdf_2d(sdf, max_cell_size).split_into_triangles(); + + // Remove triangle that fall completely outside (this is also done by voxelize_sdf for quads, + // but it may be that after splitting that some triangles still fall completely outside + let desired_cell_indices: Vec<_> = mesh + .cell_iter() + .enumerate() + .filter(|(_, triangle)| { + // TODO: Implement better criteria for filtering triangles. By only checking + // values at SDF, we may discard triangles that are intersected by the SDF but + // no vertices of the triangle are inside of the shape + triangle.0.iter().any(|v| sdf.eval(v) <= T::zero()) + }) + .map(|(cell_index, _)| cell_index) + .collect(); + + mesh = mesh.keep_cells(&desired_cell_indices); + + mesh.transform_vertices(|vertex| { + let phi = sdf.eval(vertex); + if phi > T::zero() { + let grad = sdf.gradient(vertex).expect("TODO: Fix when no gradient"); + let new_vertex = &*vertex - grad * phi; + *vertex = new_vertex; + } + }); + + mesh +} + +/// Generates an axis-aligned rectangular uniform three-dimensional hex mesh given a unit length, +/// dimensions as multipliers of the unit length and the number of cells per unit length. +/// +/// The resulting box is given by the set `[0, u * ux] x [0, u * uy] x [0, u * uz]` +/// where u denotes the unit length, ux, uy and uz denote the number of units along each +/// coordinate axis. +pub fn create_rectangular_uniform_hex_mesh( + unit_length: T, + units_x: usize, + units_y: usize, + units_z: usize, + cells_per_unit: usize, +) -> HexMesh +where + T: RealField, +{ + if cells_per_unit == 0 || units_x == 0 || units_y == 0 { + HexMesh::from_vertices_and_connectivity(Vec::new(), Vec::new()) + } else { + let mut vertices = Vec::new(); + let mut cells = Vec::new(); + + let cell_size = T::from_f64(unit_length.to_subset().unwrap() / cells_per_unit as f64).unwrap(); + + let num_cells_x = units_x * cells_per_unit; + let num_cells_y = units_y * cells_per_unit; + let num_cells_z = units_z * cells_per_unit; + let num_vertices_x = num_cells_x + 1; + let num_vertices_y = num_cells_y + 1; + let num_vertices_z = num_cells_z + 1; + + let to_global_vertex_index = + |i: usize, j: usize, k: usize| (num_vertices_x * num_vertices_y) * k + (num_vertices_x) * j + i; + + for k in 0..num_vertices_z { + for j in 0..num_vertices_y { + for i in 0..num_vertices_x { + let v = Point3::new( + T::from_usize(i).unwrap() * cell_size, + T::from_usize(j).unwrap() * cell_size, + T::from_usize(k).unwrap() * cell_size, + ); + vertices.push(v); + } + } + } + + for k in 0..num_cells_z { + for j in 0..num_cells_y { + for i in 0..num_cells_x { + let idx = &to_global_vertex_index; + cells.push(Hex8Connectivity([ + idx(i, j, k), + idx(i + 1, j, k), + idx(i + 1, j + 1, k), + idx(i, j + 1, k), + idx(i, j, k + 1), + idx(i + 1, j, k + 1), + idx(i + 1, j + 1, k + 1), + idx(i, j + 1, k + 1), + ])); + } + } + } + + Mesh::from_vertices_and_connectivity(vertices, cells) + } +} + +pub fn create_simple_stupid_sphere(center: &Point3, radius: f64, num_sweeps: usize) -> PolyMesh3d { + assert!(num_sweeps > 0); + + // Create cube centered at origin + let mut mesh = create_rectangular_uniform_hex_mesh(2.0 * radius, 1, 1, 1, 1); + mesh.translate(&Vector3::new(-radius, -radius, -radius)); + let mut mesh = PolyMesh3d::from(&mesh); + + for i_theta in 0..num_sweeps { + for j_phi in 0..num_sweeps { + let theta = PI * (i_theta as f64) / (num_sweeps as f64); + let phi = 2.0 * PI * (j_phi as f64) / (num_sweeps as f64); + let r = radius; + + let x = r * theta.sin() * phi.cos(); + let y = r * theta.sin() * phi.sin(); + let z = r * theta.cos(); + + let x = Point3::new(x, y, z); + // Normal must point inwards + let n = -x.coords.normalize(); + let half_space = HalfSpace::from_point_and_normal(x, Unit::new_normalize(n)); + mesh = mesh.intersect_half_space(&half_space); + } + } + + // Move sphere from origin to desired center + mesh.translate(¢er.coords); + mesh +} diff --git a/fenris/src/geometry/proptest_strategies.rs b/fenris/src/geometry/proptest_strategies.rs new file mode 100644 index 0000000..29f08ab --- /dev/null +++ b/fenris/src/geometry/proptest_strategies.rs @@ -0,0 +1,170 @@ +use crate::geometry::procedural::create_rectangular_uniform_quad_mesh_2d; +use crate::geometry::{LineSegment2d, Orientation, Quad2d, Triangle, Triangle2d}; +use crate::mesh::QuadMesh2d; + +use crate::util::proptest::point2_f64_strategy; +use nalgebra::{Point2, Vector2}; +use proptest::prelude::*; +use std::cmp::max; + +pub fn triangle2d_strategy_f64() -> impl Strategy> { + [point2_f64_strategy(), point2_f64_strategy(), point2_f64_strategy()].prop_map(|points| Triangle(points)) +} + +pub fn clockwise_triangle2d_strategy_f64() -> impl Strategy> { + triangle2d_strategy_f64().prop_map(|mut triangle| { + if triangle.orientation() != Orientation::Clockwise { + triangle.swap_vertices(0, 2); + } + triangle + }) +} + +pub fn nondegenerate_line_segment2d_strategy_f64() -> impl Strategy> { + // Make sure to construct the second point from non-zero components + let gen = prop_oneof![0.5..3.5, -0.5..3.5, 1e-6..10.0, -10.0..-1e-6]; + (point2_f64_strategy(), gen.clone(), gen).prop_map(|(a, x, y)| { + let d = Vector2::new(x, y); + let b = a + d; + LineSegment2d::new(a, b) + }) +} + +/// A strategy for triangles that are oriented clockwise and not degenerate +/// (i.e. collapsed to a line, area +pub fn nondegenerate_triangle2d_strategy_f64() -> impl Strategy> { + let segment = nondegenerate_line_segment2d_strategy_f64(); + let t1_gen = prop_oneof![-3.0..3.0, -10.0..10.0]; + let t2_gen = prop_oneof![0.5..3.0, 1e-6..10.0]; + (segment, t1_gen, t2_gen).prop_map(|(segment, t1, t2)| { + let a = segment.from(); + let b = segment.to(); + let ab = b - a; + let n = Vector2::new(-ab.y, ab.x); + let c = Point2::from(a + t1 * ab + t2 * n); + Triangle([*a, *b, c]) + }) +} + +fn extrude_triangle_to_convex_quad(triangle: &Triangle2d, t1: f64, t3: f64) -> Quad2d { + // In order to generate a convex quad, we first generate one triangle, + // then we "extrude" a vertex from one of the sides of the triangle, in such a way + // that the vertex is contained in the convex cone defined by the two other sides, + // constrained to lie on the side itself or further away. + // The result is a convex quad. + let t2 = 1.0 - t1; + assert!(t1 >= 0.0 && t1 <= 1.0 && t2 >= 0.0 && t3 >= 0.0); + let a = &triangle.0[0]; + let b = &triangle.0[1]; + let c = &triangle.0[2]; + let d1 = b - a; + let d2 = c - a; + // Define a vector d3 pointing from a to a point on the opposite edge + let d3_hat = t1 * d1 + t2 * d2; + // Choose a parameter t3 >= 0. Then (1 + t3) * d3_hat is a vector pointing from a to the new + // point + let d3 = (1.0 + t3) * d3_hat; + + Quad2d([*a, *b, a + d3, *c]) +} + +pub fn convex_quad2d_strategy_f64() -> impl Strategy> { + let t1_gen = 0.0..=1.0; + let t3_gen = 0.0..10.0; + (t1_gen, t3_gen, clockwise_triangle2d_strategy_f64()) + .prop_map(|(t1, t3, triangle)| extrude_triangle_to_convex_quad(&triangle, t1, t3)) +} + +pub fn nondegenerate_convex_quad2d_strategy_f64() -> impl Strategy> { + let t1_gen = prop_oneof![0.25..=0.75, 1e-6..=(1.0 - 1e-6)]; + let t3_gen = prop_oneof![0.5..3.0, 1e-6..10.0]; + (t1_gen, t3_gen, nondegenerate_triangle2d_strategy_f64()) + .prop_map(|(t1, t3, triangle)| extrude_triangle_to_convex_quad(&triangle, t1, t3)) +} + +pub fn parallelogram_strategy_f64() -> impl Strategy> { + nondegenerate_triangle2d_strategy_f64().prop_map(|triangle| { + let a = &triangle.0[0]; + let b = &triangle.0[1]; + let d = &triangle.0[2]; + let ab = b - a; + let ad = d - a; + let c = a + ab + ad; + Quad2d([*a, *b, c, *d]) + }) +} + +// Returns a strategy in which each value is a triplet (cells_per_unit, units_x, units_y) +// such that cells_per_unit^2 * units_x * units_y <= max_cells +fn rectangular_uniform_mesh_cell_distribution_strategy( + max_cells: usize, +) -> impl Strategy { + let max_cells_per_unit = f64::floor(f64::sqrt(max_cells as f64)) as usize; + (1..=max(1, max_cells_per_unit)) + .prop_flat_map(move |cells_per_unit| (Just(cells_per_unit), 0..=max_cells / (cells_per_unit * cells_per_unit))) + .prop_flat_map(move |(cells_per_unit, units_x)| { + let units_y_strategy = 0..=max_cells / (cells_per_unit * cells_per_unit * max(1, units_x)); + (Just(cells_per_unit), Just(units_x), units_y_strategy) + }) +} + +pub fn rectangular_uniform_mesh_strategy(unit_length: f64, max_cells: usize) -> impl Strategy> { + rectangular_uniform_mesh_cell_distribution_strategy(max_cells).prop_map( + move |(cells_per_unit, units_x, units_y)| { + create_rectangular_uniform_quad_mesh_2d( + unit_length, + units_x, + units_y, + cells_per_unit, + &Vector2::new(0.0, 0.0), + ) + }, + ) +} + +#[cfg(test)] +mod tests { + use super::{ + convex_quad2d_strategy_f64, nondegenerate_convex_quad2d_strategy_f64, nondegenerate_triangle2d_strategy_f64, + rectangular_uniform_mesh_cell_distribution_strategy, + }; + use crate::geometry::Orientation; + use proptest::prelude::*; + + proptest! { + #[test] + fn rectangular_uniform_mesh_cell_distribution_strategy_respects_max_cells( + (max_cells, cells_per_unit, units_x, units_y) + in (0..20usize).prop_flat_map(|max_cells| { + rectangular_uniform_mesh_cell_distribution_strategy(max_cells) + .prop_map(move |(cells_per_unit, units_x, units_y)| { + (max_cells, cells_per_unit, units_x, units_y) + }) + }) + ) { + // Test that the distribution strategy for rectangular meshes + // respects the maximum number of cells given + prop_assert!(cells_per_unit * cells_per_unit * units_x * units_y <= max_cells); + } + + #[test] + fn convex_quads_are_convex(quad in convex_quad2d_strategy_f64()) { + prop_assert!(quad.concave_corner().is_none()); + } + + #[test] + fn nondegenerate_triangles_have_positive_area( + triangle in nondegenerate_triangle2d_strategy_f64() + ){ + prop_assert!(triangle.area() > 0.0); + prop_assert!(triangle.orientation() == Orientation::Clockwise); + } + + #[test] + fn nondegenerate_quads_have_positive_area( + quad in nondegenerate_convex_quad2d_strategy_f64() + ){ + prop_assert!(quad.area() > 0.0); + } + } +} diff --git a/fenris/src/geometry/sdf.rs b/fenris/src/geometry/sdf.rs new file mode 100644 index 0000000..c90c552 --- /dev/null +++ b/fenris/src/geometry/sdf.rs @@ -0,0 +1,170 @@ +use nalgebra::{Point2, RealField, Scalar, Vector2, U2}; + +use crate::geometry::{AxisAlignedBoundingBox2d, BoundedGeometry}; +use numeric_literals::replace_float_literals; + +pub trait SignedDistanceFunction2d +where + T: Scalar, +{ + fn eval(&self, x: &Point2) -> T; + fn gradient(&self, x: &Point2) -> Option>; + + fn union(self, other: Other) -> SdfUnion + where + Self: Sized, + Other: Sized + SignedDistanceFunction2d, + { + SdfUnion { + left: self, + right: other, + } + } +} + +pub trait BoundedSdf: SignedDistanceFunction2d + BoundedGeometry +where + T: Scalar, +{ +} + +impl BoundedSdf for X +where + T: Scalar, + X: SignedDistanceFunction2d + BoundedGeometry, +{ +} + +#[derive(Copy, Clone, Debug)] +pub struct SdfCircle +where + T: Scalar, +{ + pub radius: T, + pub center: Vector2, +} + +#[derive(Copy, Clone, Debug)] +pub struct SdfUnion { + pub left: Left, + pub right: Right, +} + +#[derive(Copy, Clone, Debug)] +pub struct SdfAxisAlignedBox +where + T: Scalar, +{ + pub aabb: AxisAlignedBoundingBox2d, +} + +impl BoundedGeometry for SdfCircle +where + T: RealField, +{ + type Dimension = U2; + + fn bounding_box(&self) -> AxisAlignedBoundingBox2d { + let eps = self.radius * T::from_f64(0.01).unwrap(); + AxisAlignedBoundingBox2d::new( + self.center - Vector2::repeat(T::one()) * (self.radius + eps), + self.center + Vector2::repeat(T::one()) * (self.radius + eps), + ) + } +} + +impl SignedDistanceFunction2d for SdfCircle +where + T: RealField, +{ + fn eval(&self, x: &Point2) -> T { + let y = x - self.center; + y.coords.norm() - self.radius + } + + fn gradient(&self, x: &Point2) -> Option> { + let y = x - self.center; + let y_norm = y.coords.norm(); + + if y_norm == T::zero() { + None + } else { + Some(y.coords / y_norm) + } + } +} + +impl BoundedGeometry for SdfUnion +where + T: RealField, + Left: BoundedGeometry, + Right: BoundedGeometry, +{ + type Dimension = U2; + + fn bounding_box(&self) -> AxisAlignedBoundingBox2d { + self.left.bounding_box().enclose(&self.right.bounding_box()) + } +} + +impl SignedDistanceFunction2d for SdfUnion +where + T: RealField, + Left: SignedDistanceFunction2d, + Right: SignedDistanceFunction2d, +{ + fn eval(&self, x: &Point2) -> T { + self.left.eval(x).min(self.right.eval(x)) + } + + fn gradient(&self, x: &Point2) -> Option> { + // TODO: Is this actually correct? It might give funky results if exactly + // at points where either SDF is non-differentiable + if self.left.eval(x) < self.right.eval(x) { + self.left.gradient(x) + } else { + self.right.gradient(x) + } + } +} + +impl BoundedGeometry for SdfAxisAlignedBox +where + T: RealField, +{ + type Dimension = U2; + + fn bounding_box(&self) -> AxisAlignedBoundingBox2d { + self.aabb + } +} + +impl SignedDistanceFunction2d for SdfAxisAlignedBox +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).unwrap())] + fn eval(&self, x: &Point2) -> T { + let b = self.aabb.extents() / 2.0; + let p = x - self.aabb.center(); + let d = p.abs() - b; + + // TODO: Use d.max() when fixed. See https://github.com/rustsim/nalgebra/issues/620 + d.sup(&Vector2::zeros()).norm() + T::min(T::zero(), d[d.imax()]) + } + + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn gradient(&self, x: &Point2) -> Option> { + // TODO: Replace finite differences with "proper" gradient + // Note: arbitrary "step"/resolution h + let h = 1e-4; + let mut gradient = Vector2::zeros(); + for i in 0..2 { + let mut dx = Vector2::zeros(); + dx[i] = h; + gradient[i] = (self.eval(&(x + dx)) - self.eval(&(x - dx))) / (2.0 * h) + } + gradient.normalize_mut(); + Some(gradient) + } +} diff --git a/fenris/src/geometry/vtk.rs b/fenris/src/geometry/vtk.rs new file mode 100644 index 0000000..5042c3b --- /dev/null +++ b/fenris/src/geometry/vtk.rs @@ -0,0 +1,571 @@ +use crate::geometry::{ConvexPolygon, GeneralPolygon}; +use crate::mesh::{ClosedSurfaceMesh2d, Mesh, Mesh2d, QuadMesh2d, TriangleMesh2d}; +use nalgebra::{DefaultAllocator, DimMin, DimName, Point, RealField, Scalar}; +use num::Zero; +use std::convert::TryInto; +use std::iter::repeat; +use vtkio::model::{Attribute, Attributes, CellType, Cells, DataSet, PolyDataTopology, Version, Vtk}; +use vtkio::{export_be, Error}; + +use crate::allocators::ElementConnectivityAllocator; +use crate::connectivity::{ + Connectivity, Hex20Connectivity, Hex27Connectivity, Hex8Connectivity, Quad4d2Connectivity, Quad9d2Connectivity, + Segment2d2Connectivity, Tet10Connectivity, Tet20Connectivity, Tet4Connectivity, Tri3d2Connectivity, + Tri3d3Connectivity, Tri6d2Connectivity, +}; +use crate::element::{ElementConnectivity, FiniteElement}; +use crate::geometry::polymesh::PolyMesh; +use crate::quadrature::Quadrature; +use itertools::zip_eq; +use nalgebra::allocator::Allocator; +use std::fs::create_dir_all; +use std::path::Path; + +/// Represents connectivity that is supported by VTK. +pub trait VtkCellConnectivity: Connectivity { + fn num_nodes(&self) -> usize { + self.vertex_indices().len() + } + + fn cell_type(&self) -> vtkio::model::CellType; + + /// Write connectivity and return number of nodes. + /// + /// Panics if `connectivity.len() != self.num_nodes()`. + fn write_vtk_connectivity(&self, connectivity: &mut [usize]) { + assert_eq!(connectivity.len(), self.vertex_indices().len()); + connectivity.clone_from_slice(self.vertex_indices()); + } +} + +impl VtkCellConnectivity for Segment2d2Connectivity { + fn cell_type(&self) -> CellType { + CellType::Line + } +} + +impl VtkCellConnectivity for Tri3d2Connectivity { + fn cell_type(&self) -> CellType { + CellType::Triangle + } +} + +impl VtkCellConnectivity for Tri6d2Connectivity { + fn cell_type(&self) -> CellType { + CellType::QuadraticTriangle + } +} + +impl VtkCellConnectivity for Quad4d2Connectivity { + fn cell_type(&self) -> CellType { + CellType::Quad + } +} + +impl VtkCellConnectivity for Quad9d2Connectivity { + fn cell_type(&self) -> CellType { + CellType::QuadraticQuad + } +} + +impl VtkCellConnectivity for Tet4Connectivity { + fn cell_type(&self) -> CellType { + CellType::Tetra + } +} + +impl VtkCellConnectivity for Hex8Connectivity { + fn cell_type(&self) -> CellType { + CellType::Hexahedron + } +} + +impl VtkCellConnectivity for Tri3d3Connectivity { + fn cell_type(&self) -> CellType { + CellType::Triangle + } +} + +impl VtkCellConnectivity for Tet10Connectivity { + fn cell_type(&self) -> CellType { + CellType::QuadraticTetra + } + + fn write_vtk_connectivity(&self, connectivity: &mut [usize]) { + assert_eq!(connectivity.len(), self.vertex_indices().len()); + connectivity.clone_from_slice(self.vertex_indices()); + + // Gmsh ordering and ParaView have different conventions for quadratic tets, + // so we must adjust for that. In particular, nodes 8 and 9 are switched + connectivity.swap(8, 9); + } +} + +// Note: There is no Tet20 in ParaView (legacy anyway), +// so we only export it as a Tet4 element +impl VtkCellConnectivity for Tet20Connectivity { + fn num_nodes(&self) -> usize { + 4 + } + + fn cell_type(&self) -> CellType { + CellType::Tetra + } + + fn write_vtk_connectivity(&self, connectivity: &mut [usize]) { + assert_eq!(connectivity.len(), self.num_nodes()); + connectivity.clone_from_slice(&self.vertex_indices()[0..4]); + } +} + +impl VtkCellConnectivity for Hex20Connectivity { + fn cell_type(&self) -> CellType { + CellType::QuadraticHexahedron + } + + fn write_vtk_connectivity(&self, connectivity: &mut [usize]) { + assert_eq!(connectivity.len(), self.num_nodes()); + + let v = self.vertex_indices(); + // The first 8 entries are the same + connectivity[0..8].clone_from_slice(&v[0..8]); + connectivity[8] = v[8]; + connectivity[9] = v[11]; + connectivity[10] = v[13]; + connectivity[11] = v[9]; + connectivity[12] = v[16]; + connectivity[13] = v[18]; + connectivity[14] = v[19]; + connectivity[15] = v[17]; + connectivity[16] = v[10]; + connectivity[17] = v[12]; + connectivity[18] = v[14]; + connectivity[19] = v[15]; + } +} + +impl VtkCellConnectivity for Hex27Connectivity { + fn num_nodes(&self) -> usize { + 20 + } + + // There is no tri-quadratic Hex in legacy VTK, so use Hex20 instead + fn cell_type(&self) -> CellType { + CellType::QuadraticHexahedron + } + + fn write_vtk_connectivity(&self, connectivity: &mut [usize]) { + assert_eq!(connectivity.len(), self.num_nodes()); + + let v = self.vertex_indices(); + // The first 8 entries are the same + connectivity[0..8].clone_from_slice(&v[0..8]); + connectivity[8] = v[8]; + connectivity[9] = v[11]; + connectivity[10] = v[13]; + connectivity[11] = v[9]; + connectivity[12] = v[16]; + connectivity[13] = v[18]; + connectivity[14] = v[19]; + connectivity[15] = v[17]; + connectivity[16] = v[10]; + connectivity[17] = v[12]; + connectivity[18] = v[14]; + connectivity[19] = v[15]; + } +} + +impl<'a, T, D, C> From<&'a Mesh> for DataSet +where + T: Scalar + Zero, + D: DimName, + C: VtkCellConnectivity, + DefaultAllocator: Allocator, +{ + fn from(mesh: &'a Mesh) -> Self { + // TODO: Create a "SmallDim" trait or something for this case...? + // Or just implement the trait directly for U1/U2/U3? + assert!(D::dim() <= 3, "Unable to support dimensions larger than 3."); + let points: Vec<_> = { + let mut points: Vec = Vec::new(); + for v in mesh.vertices() { + points.extend_from_slice(v.coords.as_slice()); + + for _ in v.coords.len()..3 { + points.push(T::zero()); + } + } + points + }; + + // Vertices is laid out as follows: N, i_1, i_2, ... i_N, + // so for quads this becomes 4 followed by the four indices making up the quad + let mut vertices = Vec::new(); + let mut cell_types = Vec::new(); + let mut vertex_indices = Vec::new(); + for cell in mesh.connectivity() { + // TODO: Return Result or something + vertices.push(cell.num_nodes() as u32); + + vertex_indices.clear(); + vertex_indices.resize(cell.num_nodes(), 0); + cell.write_vtk_connectivity(&mut vertex_indices); + + // TODO: Safer cast? How to handle this? TryFrom instead of From? + vertices.extend(vertex_indices.iter().copied().map(|i| i as u32)); + cell_types.push(cell.cell_type()); + } + + DataSet::UnstructuredGrid { + points: points.into(), + cells: Cells { + num_cells: mesh.connectivity().len() as u32, + vertices, + }, + cell_types, + data: Attributes::new(), + } + } +} + +impl<'a, T, D> From<&'a PolyMesh> for DataSet +where + T: Scalar + Zero, + D: DimName, + DefaultAllocator: Allocator, +{ + fn from(mesh: &'a PolyMesh) -> Self { + assert!(D::dim() == 2 || D::dim() == 3, "Only dimensions 2 and 3 supported."); + + let points: Vec<_> = { + let mut points: Vec = Vec::new(); + for v in mesh.vertices() { + points.extend_from_slice(v.coords.as_slice()); + + if D::dim() == 2 { + points.push(T::zero()); + } + } + points + }; + + // Vertices is laid out as follows: N, i_1, i_2, ... i_N, + // so for quads this becomes 4 followed by the four indices making up the quad + let mut vertices = Vec::new(); + for face in mesh.face_connectivity_iter() { + vertices.push(face.len() as u32); + for idx in face { + // TODO: Safer cast? How to handle this? TryFrom instead of From? + vertices.push(*idx as u32); + } + } + + let cells = Cells { + num_cells: mesh.num_faces() as u32, + vertices, + }; + + DataSet::PolyData { + points: points.into(), + topo: vec![PolyDataTopology::Polygons(cells)], + data: Attributes::new(), + } + } +} + +impl<'a, T> From<&'a ClosedSurfaceMesh2d> for DataSet +where + T: Scalar + Zero, +{ + fn from(mesh: &'a ClosedSurfaceMesh2d) -> Self { + Self::from(mesh.mesh()) + } +} + +impl<'a, T> From<&'a GeneralPolygon> for DataSet +where + T: RealField, +{ + fn from(polygon: &'a GeneralPolygon) -> Self { + let mut points = Vec::with_capacity(polygon.num_vertices() * 3); + let mut cells = Cells { + num_cells: polygon.num_edges() as u32, + vertices: Vec::new(), + }; + + for v in polygon.vertices() { + points.push(v.x); + points.push(v.y); + points.push(T::zero()); + } + + for i in 0..polygon.num_edges() { + cells.vertices.push(2); + // Edge points from vertex i to i + 1 (modulo) + cells.vertices.push(i as u32); + cells.vertices.push(((i + 1) % polygon.num_edges()) as u32); + } + + DataSet::PolyData { + points: points.into(), + topo: vec![PolyDataTopology::Lines(cells)], + data: Attributes::new(), + } + } +} + +pub fn create_vtk_data_set_from_quadratures( + vertices: &[Point], + connectivity: &[C], + quadrature_rules: impl IntoIterator>, +) -> DataSet +where + T: RealField, + D: DimName + DimMin, + C: ElementConnectivity, + DefaultAllocator: Allocator + ElementConnectivityAllocator, +{ + let quadrature_rules = quadrature_rules.into_iter(); + + // Quadrature weights and points mapped to physical domain + let mut physical_weights = Vec::new(); + let mut physical_points = Vec::new(); + // Cell indices map each individual quadrature point to its original cell + let mut cell_indices = Vec::new(); + + for ((cell_idx, conn), quadrature) in zip_eq(connectivity.iter().enumerate(), quadrature_rules) { + let element = conn.element(vertices).unwrap(); + for (w_ref, xi) in zip_eq(quadrature.weights(), quadrature.points()) { + let j = element.reference_jacobian(xi); + let x = element.map_reference_coords(xi); + let w_physical = j.determinant().abs() * *w_ref; + physical_points.push(Point::from(x)); + physical_weights.push(w_physical); + cell_indices.push(cell_idx as u64); + } + } + + // let (new_weights, new_points): (Vec<_>, Vec<_>) = connectivity + // .iter() + // .enumerate() + // .zip_eq(quadrature_rules) + // .flat_map(|((cell_idx, conn), quadrature)| { + // let element = conn.element(vertices).unwrap(); + // let quadrature = zip_eq(quadrature.weights(), quadrature.points()) + // .map(|(w_ref, xi)| { + // let j = element.reference_jacobian(xi); + // let x = element.map_reference_coords(xi); + // let w_physical = j.determinant().abs() * *w_ref; + // (w_physical, Point::from(x)) + // }) + // .collect::>(); + // quadrature + // }) + // .unzip(); + + let mut dataset = create_vtk_data_set_from_points(&physical_points); + let weight_point_attributes = Attribute::Scalars { + num_comp: 1, + lookup_table: None, + data: physical_weights.into(), + }; + + let cell_idx_point_attributes = Attribute::Scalars { + num_comp: 1, + lookup_table: None, + data: cell_indices.into(), + }; + + match dataset { + DataSet::PolyData { ref mut data, .. } => { + data.point + .push(("weight".to_string(), weight_point_attributes)); + data.point + .push(("cell_index".to_string(), cell_idx_point_attributes)); + } + _ => panic!("Unexpected enum variant from data set."), + } + + dataset +} + +/// TODO: Remove in favor of `From` +pub fn create_vtk_data_set_from_quad_mesh(mesh: &QuadMesh2d) -> DataSet +where + T: Scalar + Zero, +{ + let points: Vec<_> = { + let mut points: Vec = Vec::new(); + for v in mesh.vertices() { + points.extend_from_slice(v.coords.as_slice()); + points.push(T::zero()); + } + points + }; + + // Vertices is laid out as follows: N, i_1, i_2, ... i_N, + // so for quads this becomes 4 followed by the four indices making up the quad + let mut vertices = Vec::new(); + vertices.reserve(5 * mesh.connectivity().len()); + for cell in mesh.connectivity() { + // TODO: Return Result or something + vertices.push(4); + vertices.push(cell[0].try_into().unwrap()); + vertices.push(cell[1].try_into().unwrap()); + vertices.push(cell[2].try_into().unwrap()); + vertices.push(cell[3].try_into().unwrap()); + } + + DataSet::UnstructuredGrid { + points: points.into(), + cells: Cells { + num_cells: mesh.connectivity().len() as u32, + vertices, + }, + cell_types: repeat(CellType::Quad) + .take(mesh.connectivity().len()) + .collect(), + data: Attributes::new(), + } +} + +/// TODO: Remove in favor of `From` +pub fn create_vtk_data_set_from_triangle_mesh(mesh: &TriangleMesh2d) -> DataSet +where + T: Scalar + Zero, +{ + let points: Vec<_> = { + let mut points: Vec = Vec::new(); + for v in mesh.vertices() { + points.extend_from_slice(v.coords.as_slice()); + points.push(T::zero()); + } + points + }; + + // Vertices is laid out as follows: N, i_1, i_2, ... i_N, + // so for triangles this becomes 3 followed by the three indices making up the triangle + let mut vertices = Vec::new(); + vertices.reserve(4 * mesh.connectivity().len()); + for cell in mesh.connectivity() { + vertices.push(3); + vertices.push(cell[0].try_into().unwrap()); + vertices.push(cell[1].try_into().unwrap()); + vertices.push(cell[2].try_into().unwrap()); + } + + DataSet::UnstructuredGrid { + points: points.into(), + cells: Cells { + num_cells: mesh.connectivity().len() as u32, + vertices, + }, + cell_types: repeat(CellType::Triangle) + .take(mesh.connectivity().len()) + .collect(), + data: Attributes::new(), + } +} + +pub fn create_vtk_data_set_from_polygons(polygons: &[ConvexPolygon]) -> DataSet +where + T: Scalar + Zero, +{ + let mut points = Vec::new(); + let mut cells = Cells { + num_cells: polygons.len() as u32, + vertices: Vec::new(), + }; + + for polygon in polygons { + let point_start = (points.len() / 3) as u32; + let num_points = polygon.vertices().len() as u32; + + cells.vertices.push(num_points); + + for (i, vertex) in polygon.vertices().iter().enumerate() { + points.push(vertex.x.clone()); + points.push(vertex.y.clone()); + points.push(T::zero()); + cells.vertices.push(point_start + i as u32); + } + } + + DataSet::PolyData { + points: points.into(), + topo: vec![PolyDataTopology::Polygons(cells)], + data: Attributes::new(), + } +} + +pub fn create_vtk_data_set_from_points(points: &[Point]) -> DataSet +where + T: Scalar + Zero, + D: DimName, + DefaultAllocator: Allocator, +{ + assert!(D::dim() <= 3, "Only support dimensions up to 3."); + + let mut vtk_points = Vec::new(); + let mut cells = Cells { + num_cells: points.len() as u32, + vertices: Vec::new(), + }; + + for (i, point) in points.iter().enumerate() { + for j in 0..D::dim() { + vtk_points.push(point.coords[j].clone()); + } + + for _ in D::dim()..3 { + vtk_points.push(T::zero()); + } + + cells.vertices.push(1); + cells.vertices.push(i as u32); + } + + DataSet::PolyData { + points: vtk_points.into(), + topo: vec![PolyDataTopology::Vertices(cells)], + data: Attributes::new(), + } +} + +/// Convenience method for easily writing polygons to VTK files +pub fn write_vtk_polygons(polygons: &[ConvexPolygon], filename: &str, title: &str) -> Result<(), Error> +where + T: Scalar + Zero, +{ + let data = create_vtk_data_set_from_polygons(polygons); + write_vtk(data, filename, title) +} + +/// Convenience function for writing meshes to VTK files. +pub fn write_vtk_mesh<'a, T, Connectivity>( + mesh: &'a Mesh2d, + filename: &str, + title: &str, +) -> Result<(), Error> +where + T: Scalar + Zero, + &'a Mesh2d: Into, +{ + let data = mesh.into(); + write_vtk(data, filename, title) +} + +pub fn write_vtk>(data: impl Into, filename: P, title: &str) -> Result<(), Error> { + let vtk_file = Vtk { + version: Version::new((4, 1)), + title: title.to_string(), + data: data.into(), + }; + + let filename = filename.as_ref(); + + if let Some(dir) = filename.parent() { + create_dir_all(dir)?; + } + export_be(vtk_file, filename) +} diff --git a/fenris/src/lib.rs b/fenris/src/lib.rs new file mode 100644 index 0000000..0d0b04c --- /dev/null +++ b/fenris/src/lib.rs @@ -0,0 +1,33 @@ +pub mod allocators; +pub mod assembly; +pub mod cg; +pub mod connectivity; +pub mod element; +pub mod embedding; +pub mod error; +pub mod geometry; +pub mod lp_solvers; +pub mod model; +pub mod quadrature; +pub mod reorder; +pub mod rtree; +pub mod solid; +pub mod space; +pub mod sparse; +pub mod util; + +#[cfg(feature = "proptest")] +pub mod proptest; + +// TODO: Don't export +pub use sparse::CooMatrix; +pub use sparse::CsrMatrix; + +pub mod mesh; + +mod mesh_convert; +mod space_impl; + +pub extern crate nalgebra; +pub extern crate nested_vec; +pub extern crate vtkio; diff --git a/fenris/src/lp_solvers.rs b/fenris/src/lp_solvers.rs new file mode 100644 index 0000000..de1992e --- /dev/null +++ b/fenris/src/lp_solvers.rs @@ -0,0 +1,57 @@ +use crate::embedding::LpSolver; +use crate::nalgebra::{DMatrix, DVector}; +use lp_bfp::{solve_lp, Verbosity}; +use std::error::Error; +use std::f64; + +/// A basic feasible point solver powered by Google's GLOP LP solver. +#[derive(Debug, Clone)] +pub struct GlopSolver { + verbosity: Verbosity, +} + +impl GlopSolver { + pub fn new() -> Self { + Self { + verbosity: Verbosity::NoVerbose, + } + } + + pub fn new_verbose() -> Self { + Self { + verbosity: Verbosity::Verbose, + } + } +} + +impl LpSolver for GlopSolver { + fn solve_lp( + &self, + c: &DVector, + a: &DMatrix, + b: &DVector, + lb: &[Option], + ub: &[Option], + ) -> Result, Box> { + let a_elements_row_major: Vec<_> = a.transpose().iter().copied().collect(); + let lb: Vec<_> = lb + .iter() + .copied() + .map(|lb_i| lb_i.unwrap_or(-f64::INFINITY)) + .collect(); + let ub: Vec<_> = ub + .iter() + .copied() + .map(|ub_i| ub_i.unwrap_or(f64::INFINITY)) + .collect(); + let bfp = solve_lp( + c.as_slice(), + &a_elements_row_major, + b.as_slice(), + &lb, + &ub, + self.verbosity, + )?; + Ok(DVector::from_column_slice(&bfp)) + } +} diff --git a/fenris/src/mesh.rs b/fenris/src/mesh.rs new file mode 100644 index 0000000..9277584 --- /dev/null +++ b/fenris/src/mesh.rs @@ -0,0 +1,744 @@ +use crate::geometry::{ + AxisAlignedBoundingBox, BoundedGeometry, Distance, DistanceQuery, GeneralPolygon, GeometryCollection, + LineSegment2d, Polygon, SignedDistance, SignedDistanceResult, +}; +use arrayvec::ArrayVec; +use nalgebra::{DefaultAllocator, DimName, Point, Point2, RealField, Scalar, Vector2, VectorN, U2, U3}; +use nested_vec::NestedVec; +use std::collections::hash_map::Entry as HashMapEntry; +use std::collections::{BTreeMap, HashMap}; +use std::iter::once; + +use numeric_literals::replace_float_literals; + +use crate::connectivity::{ + CellConnectivity, Connectivity, ConnectivityMut, Hex20Connectivity, Hex27Connectivity, Hex8Connectivity, + Quad4d2Connectivity, Quad9d2Connectivity, Segment2d2Connectivity, Tet10Connectivity, Tet20Connectivity, + Tet4Connectivity, Tri3d2Connectivity, Tri3d3Connectivity, Tri6d2Connectivity, +}; +use crate::geometry::Orientation::Counterclockwise; +use nalgebra::allocator::Allocator; +use serde::{Deserialize, Serialize}; +use std::cmp::min; +use std::error::Error; + +/// Index-based data structure for conforming meshes (i.e. no hanging nodes). +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct Mesh +where + D: DimName, + DefaultAllocator: Allocator, +{ + // serde's not able correctly determine the necessary trait bounds in this case, + // so write our own + #[serde(bound( + serialize = ">::Buffer: Serialize", + deserialize = ">::Buffer: Deserialize<'de>" + ))] + vertices: Vec>, + #[serde(bound( + serialize = "Connectivity: Serialize", + deserialize = "Connectivity: Deserialize<'de>" + ))] + connectivity: Vec, +} + +/// Index-based data structure for conforming meshes (i.e. no hanging nodes). +pub type Mesh2d = Mesh; +pub type Mesh3d = Mesh; + +pub type TriangleMesh2d = Mesh2d; +pub type Tri6Mesh2d = Mesh2d; +pub type QuadMesh2d = Mesh2d; +pub type Quad9Mesh2d = Mesh2d; +pub type TriangleMesh3d = Mesh3d; +// TODO: Rename to Hex8Mesh +pub type HexMesh = Mesh3d; +pub type Hex20Mesh = Mesh3d; +pub type Hex27Mesh = Mesh3d; +pub type Tet4Mesh = Mesh3d; +pub type Tet10Mesh = Mesh3d; +pub type Tet20Mesh = Mesh3d; + +impl Mesh +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, +{ + pub fn vertices_mut(&mut self) -> &mut [Point] { + &mut self.vertices + } + + pub fn vertices(&self) -> &[Point] { + &self.vertices + } + + pub fn connectivity(&self) -> &[Connectivity] { + &self.connectivity + } + + /// Construct a mesh from vertices and connectivity. + /// + /// The provided connectivity is expected only to return valid (i.e. in-bounds) indices, + /// but this can not be trusted. Users of the mesh are permitted to panic if they encounter + /// invalid indices, but unchecked indexing may easily lead to undefined behavior. + /// + /// In other words, if the connectivity references indices out of bounds, then the code is + /// incorrect. However, since this can be done exclusively with safe code, unchecked + /// or unsafe indexing in which the user is *trusted* to provide valid indices may + /// produce undefined behavior.Therefore, the connectivity must always be checked. + pub fn from_vertices_and_connectivity(vertices: Vec>, connectivity: Vec) -> Self { + Self { vertices, connectivity } + } +} + +impl Mesh +where + T: Scalar, + D: DimName, + C: ConnectivityMut, + DefaultAllocator: Allocator, +{ + /// Creates a new mesh with each cell disconnected from all its neighbors. + /// + /// In other words, each vertex is only referenced exactly once, and the result is + /// effectively a "soup" of cells. + pub fn disconnect_cells(&self) -> Self { + let old_vertices = self.vertices(); + let mut new_vertices = Vec::new(); + let mut new_connectivity = Vec::new(); + + for conn in self.connectivity() { + let mut new_conn = conn.clone(); + + for v_idx in new_conn.vertex_indices_mut() { + let new_vertex_idx = new_vertices.len(); + new_vertices.push(old_vertices[*v_idx].clone()); + *v_idx = new_vertex_idx; + } + new_connectivity.push(new_conn); + } + + Self::from_vertices_and_connectivity(new_vertices, new_connectivity) + } +} + +impl Mesh +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, + Connectivity: CellConnectivity, +{ + pub fn get_cell(&self, index: usize) -> Option { + self.connectivity() + .get(index) + .and_then(|conn| conn.cell(self.vertices())) + } + + pub fn cell_iter<'a>(&'a self) -> impl 'a + Iterator { + self.connectivity().iter().map(move |connectivity| { + connectivity + .cell(&self.vertices) + .expect("Mesh2d is not allowed to contain cells with indices out of bounds.") + }) + } +} + +impl Mesh +where + T: Scalar, + D: DimName, + C: Connectivity, + C::FaceConnectivity: Connectivity, + DefaultAllocator: Allocator, +{ + /// Finds cells that have at least one boundary face. + pub fn find_boundary_cells(&self) -> Vec { + let mut cells: Vec<_> = self + .find_boundary_faces() + .into_iter() + .map(|(_, cell_index, _)| cell_index) + .collect(); + cells.sort_unstable(); + cells.dedup(); + cells + } + + /// Finds faces which are only connected to exactly one cell, along with the connected cell + /// index and the local index of the face within that cell. + pub fn find_boundary_faces(&self) -> Vec<(C::FaceConnectivity, usize, usize)> { + let mut sorted_slices = NestedVec::new(); + let mut face_info = Vec::new(); + + // We want to use (sorted) slices as keys in a hash map, so we need to store + // and sort the slices first + for (conn_idx, cell_conn) in self.connectivity.iter().enumerate() { + let num_faces = cell_conn.num_faces(); + for local_idx in 0..num_faces { + let face_conn = cell_conn.get_face_connectivity(local_idx).unwrap(); + sorted_slices.push(face_conn.vertex_indices()); + let indices = sorted_slices.last_mut().unwrap(); + indices.sort_unstable(); + face_info.push((face_conn, conn_idx, local_idx)); + } + } + + // Count the number of occurrences of "equivalent" faces (in the sense that they refer + // to the same vertex indices). Use a BTreeMap to avoid non-determinism due to + // HashMap's internal randomization. + let mut slice_counts = BTreeMap::new(); + let num_slices = sorted_slices.len(); + for i in 0..num_slices { + slice_counts + .entry(sorted_slices.get(i).unwrap()) + .and_modify(|(_, count)| *count += 1) + .or_insert((i, 1)); + } + + // Take only the faces which have a count of 1, which correspond to boundary faces + slice_counts + .into_iter() + .map(|(_key, value)| value) + .filter(|&(_, count)| count == 1) + .map(move |(i, _)| face_info[i].clone()) + .collect() + } + + /// Returns a sorted list of vertices that are determined to be on the boundary. + /// + /// A vertex is considered to be a part of the boundary if it belongs to a boundary face. + pub fn find_boundary_vertices(&self) -> Vec { + let mut indices = Vec::new(); + for (connectivity, _, _) in self.find_boundary_faces() { + indices.extend(connectivity.vertex_indices()); + } + indices.sort_unstable(); + indices.dedup(); + indices + } +} + +impl BoundedGeometry for Mesh +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator, + Connectivity: CellConnectivity, + Connectivity::Cell: BoundedGeometry, +{ + type Dimension = D; + + fn bounding_box(&self) -> AxisAlignedBoundingBox { + let mut bbs = self.cell_iter().map(|cell| cell.bounding_box()); + bbs.next() + .map(|first_bb| bbs.fold(first_bb, |bb1, bb2| bb1.enclose(&bb2))) + .unwrap_or_else(|| AxisAlignedBoundingBox::new(VectorN::zeros(), VectorN::zeros())) + } +} + +impl Mesh +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator, +{ + /// Translates all vertices of the mesh by the given translation vector. + pub fn translate(&mut self, translation: &VectorN) { + self.transform_vertices(|p| *p += translation); + } + + /// Transform all vertices of the mesh by the given transformation function. + pub fn transform_vertices(&mut self, mut transformation: F) + where + F: FnMut(&mut Point), + { + for p in &mut self.vertices { + transformation(p); + } + } + + pub fn transform_all_vertices(&mut self, mut transformation: F) + where + F: FnMut(&mut [Point]), + { + transformation(&mut self.vertices); + } +} + +const EMPTY_SLICE: &[usize] = &[]; + +impl Connectivity for () { + type FaceConnectivity = (); + + fn num_faces(&self) -> usize { + 0 + } + + fn get_face_connectivity(&self, _index: usize) -> Option { + None + } + + fn vertex_indices(&self) -> &[usize] { + &EMPTY_SLICE + } +} + +impl QuadMesh2d +where + T: RealField, +{ + pub fn split_into_triangles(self) -> TriangleMesh2d { + let triangles = self + .connectivity() + .iter() + .flat_map(|connectivity| { + let Quad4d2Connectivity(c) = connectivity; + let quad = connectivity + .cell(self.vertices()) + .expect("Indices must be in bounds"); + let (tri1, tri2) = quad.split_into_triangle_connectivities(); + let tri1_global = Tri3d2Connectivity([c[tri1[0]], c[tri1[1]], c[tri1[2]]]); + let tri2_global = Tri3d2Connectivity([c[tri2[0]], c[tri2[1]], c[tri2[2]]]); + once(tri1_global).chain(once(tri2_global)) + }) + .collect(); + + TriangleMesh2d::from_vertices_and_connectivity(self.vertices, triangles) + } +} + +impl Mesh +where + T: Scalar, + D: DimName, + C: ConnectivityMut, + DefaultAllocator: Allocator, +{ + /// Returns a new mesh in which only the desired cells are kept. The vertices are removed or + /// relabeled as necessary. + pub fn keep_cells(&self, cell_indices: &[usize]) -> Self { + // TODO: Return Result instead of panicking if indices are out of bounds + + // Each entry is true if this vertex should be kept, false otherwise + let vertex_keep_table = { + let mut table = vec![false; self.vertices.len()]; + for cell_index in cell_indices { + let cell_connectivity = &self.connectivity[*cell_index]; + + for vertex_index in cell_connectivity.vertex_indices() { + table[*vertex_index] = true; + } + } + table + }; + + let old_to_new_label_map = { + let mut label_map = HashMap::new(); + let mut next_label = 0; + for (i, keep) in vertex_keep_table.iter().enumerate() { + if *keep { + label_map.insert(i, next_label); + next_label += 1; + } + } + label_map + }; + + let relabeled_cells: Vec<_> = cell_indices + .iter() + .map(|i| self.connectivity()[*i].clone()) + .map(|mut cell| { + for index in cell.vertex_indices_mut() { + *index = *old_to_new_label_map + .get(index) + .expect("Index must be in map"); + } + cell + }) + .collect(); + + let relabeled_vertices: Vec<_> = vertex_keep_table + .iter() + .enumerate() + .filter_map(|(i, should_keep)| if *should_keep { Some(i) } else { None }) + .map(|index| self.vertices[index].clone()) + .collect(); + + Mesh::from_vertices_and_connectivity(relabeled_vertices, relabeled_cells) + } +} + +impl Mesh2d +where + T: RealField, + Cell: Connectivity, +{ + pub fn extract_contour(&self) -> Result, Box> { + let boundary_edges = self + .find_boundary_faces() + .into_iter() + .map(|(edge, _, _)| edge); + + // For a "proper" mesh, any vertex may be connected to exactly two other vertices. + // We build a "path" of vertices by associating each vertex with its neighbor + // whose index is the smallest, and visiting each vertex once. + let mut neighbors = HashMap::new(); + let mut smallest_index = std::usize::MAX; + + let mut insert_neighbor = |vertex_index, neighbor_index| { + if vertex_index == neighbor_index { + Err(format!( + "Cannot extract contour: vertex {} has edge to itself.", + vertex_index + )) + } else { + neighbors + .entry(vertex_index) + .or_insert_with(|| ArrayVec::<[_; 2]>::new()) + .try_push(neighbor_index) + .map_err(|_| { + format!( + "Cannot extract contour: vertex {} has more than two neighbors.", + vertex_index + ) + }) + } + }; + + for edge in boundary_edges { + let Segment2d2Connectivity([a, b]) = edge; + insert_neighbor(a, b)?; + insert_neighbor(b, a)?; + smallest_index = min(smallest_index, a); + smallest_index = min(smallest_index, b); + } + + let num_vertices = neighbors.len(); + let mut take_next = |vertex_index, prev_index| { + debug_assert_ne!(vertex_index, prev_index); + let vertex_neighbors = neighbors + .get_mut(&vertex_index) + .expect("All vertices have neighbors"); + + const ERROR_MSG: &str = "Cannot extract contour: There is no closed path connecting vertices."; + + if vertex_neighbors.is_empty() { + Err(ERROR_MSG) + } else { + let neighbor_idx = vertex_neighbors + .iter() + .cloned() + .enumerate() + .filter(|(_, vertex_idx)| *vertex_idx != prev_index) + .map(|(i, _)| i) + .next(); + + if let Some(neighbor_idx) = neighbor_idx { + let neighbor = vertex_neighbors[neighbor_idx]; + vertex_neighbors.remove(neighbor_idx); + Ok(neighbor) + } else { + Err(ERROR_MSG) + } + } + }; + + // Given a current vertex and the previous vertex, we find the next vertex by + // picking the neighbor of "current" which is not equal to the previous. + // In order to start this sequence, we must first choose an arbitrary "next" vertex + // out of the two neighbors of "prev" + let mut vertices = Vec::with_capacity(num_vertices); + let mut prev_vertex_index = smallest_index; + let mut current_vertex_index = take_next(prev_vertex_index, std::usize::MAX)?; + vertices.push(self.vertices()[prev_vertex_index]); + + while current_vertex_index != smallest_index { + let next_vertex_index = take_next(current_vertex_index, prev_vertex_index)?; + prev_vertex_index = current_vertex_index; + current_vertex_index = next_vertex_index; + vertices.push(self.vertices()[prev_vertex_index]); + } + + // TODO: What if we have a hole in the polygon? Should eventually also support this, + // but for the moment we are limited to simple polygons. + let mut polygon = GeneralPolygon::from_vertices(vertices); + polygon.orient(Counterclockwise); + + Ok(polygon) + } +} + +impl Mesh +where + T: Scalar, + D: DimName, + C: Connectivity, + C::FaceConnectivity: Connectivity + ConnectivityMut, + DefaultAllocator: Allocator, +{ + /// Creates a mesh that consists of all unique faces of this mesh. + /// Face normals are only preserved for boundary faces. + pub fn extract_face_soup(&self) -> Mesh { + let mut unique_connectivity = HashMap::new(); + let mut faces = Vec::new(); + + for cell_conn in self.connectivity.iter() { + let num_faces = cell_conn.num_faces(); + for i in 0..num_faces { + let face_conn = cell_conn.get_face_connectivity(i).unwrap(); + + let mut vertex_indices = face_conn.vertex_indices().to_vec(); + vertex_indices.sort_unstable(); + + if let HashMapEntry::Vacant(entry) = unique_connectivity.entry(vertex_indices) { + entry.insert(faces.len()); + faces.push(face_conn); + } + } + } + + let new_mesh = Mesh::from_vertices_and_connectivity(self.vertices.clone(), faces); + let cells_to_keep: Vec<_> = (0..new_mesh.connectivity().len()).collect(); + // Remove unconnected vertices + new_mesh.keep_cells(&cells_to_keep) + } +} + +impl Mesh +where + T: Scalar, + D: DimName, + C: Connectivity, + C::FaceConnectivity: ConnectivityMut, + DefaultAllocator: Allocator, +{ + /// Constructs a new mesh from the surface cells of the mesh. + /// + /// The orientation of the faces are preserved. + pub fn extract_surface_mesh(&self) -> Mesh { + let connectivity = self + .find_boundary_faces() + .into_iter() + .map(|(face, _, _)| face) + .collect(); + + // TODO: This is rather inefficient + let new_mesh = Mesh::from_vertices_and_connectivity(self.vertices.clone(), connectivity); + let cells_to_keep: Vec<_> = (0..new_mesh.connectivity().len()).collect(); + new_mesh.keep_cells(&cells_to_keep) + } +} + +impl<'a, T, D, C> GeometryCollection<'a> for Mesh +where + T: Scalar, + D: DimName, + C: CellConnectivity, + DefaultAllocator: Allocator, +{ + type Geometry = C::Cell; + + fn num_geometries(&self) -> usize { + self.connectivity.len() + } + + fn get_geometry(&'a self, index: usize) -> Option { + self.connectivity() + .get(index) + .map(|conn| conn.cell(self.vertices()).unwrap()) + } +} + +impl<'a, T, D, C, QueryGeometry> DistanceQuery<'a, QueryGeometry> for Mesh +where + T: RealField, + D: DimName, + C: CellConnectivity, + C::Cell: Distance, + DefaultAllocator: Allocator, +{ + fn nearest(&'a self, query_geometry: &QueryGeometry) -> Option { + let (_, min_index) = (0..self.num_geometries()) + .map(|idx| { + let geometry = self + .get_geometry(idx) + .expect("num_geometries must report the correct number of available geometries"); + (idx, geometry) + }) + .fold( + (T::max_value(), None), + |(mut min_dist, mut min_index), (idx, geometry)| { + let dist = geometry.distance(query_geometry); + // TODO: Square distance? + if dist < min_dist { + min_index = Some(idx); + min_dist = dist; + } + (min_dist, min_index) + }, + ); + min_index + } +} + +pub trait PlanarFace +where + T: Scalar, + DefaultAllocator: Allocator, +{ + type Dimension: DimName; + + fn normal(&self) -> VectorN; +} + +impl PlanarFace for LineSegment2d +where + T: RealField, +{ + type Dimension = U2; + + fn normal(&self) -> Vector2 { + self.normal_dir().normalize() + } +} + +/// A closed surface mesh with a well-defined inside and outside. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct ClosedSurfaceMesh2d +where + T: Scalar, +{ + mesh: Mesh2d, + vertex_normals: Vec>, +} + +impl ClosedSurfaceMesh2d +where + T: Scalar, +{ + pub fn mesh(&self) -> &Mesh2d { + &self.mesh + } +} + +impl ClosedSurfaceMesh2d +where + T: RealField, + Connectivity: CellConnectivity, + Connectivity::Cell: PlanarFace, + DefaultAllocator: Allocator>::Dimension>, +{ + /// Transform all vertices of the mesh by the given transformation function. + pub fn transform_vertices(&mut self, mut transformation: F) + where + F: FnMut(&mut [Point2]), + { + transformation(self.mesh.vertices_mut()); + self.recompute_vertex_normals(); + } + + pub fn from_mesh(mesh: Mesh2d) -> Result> { + let num_vertices = mesh.vertices().len(); + let mut closed_mesh = Self { + mesh, + vertex_normals: vec![Vector2::zeros(); num_vertices], + }; + + closed_mesh.recompute_vertex_normals(); + + Ok(closed_mesh) + } + + fn recompute_vertex_normals(&mut self) { + for v in &mut self.vertex_normals { + *v = Vector2::zeros(); + } + + let mesh = &self.mesh; + + for conn in mesh.connectivity() { + let cell = conn.cell(mesh.vertices()).unwrap(); + let n = cell.normal(); + for v_idx in conn.vertex_indices() { + self.vertex_normals[*v_idx] += &n; + } + } + + for v in &mut self.vertex_normals { + let v_norm = v.norm(); + if v_norm > T::zero() { + *v /= v_norm; + } else { + // TODO: How to handle this situation? + *v = Vector2::zeros(); + } + } + } +} + +impl<'a, T, C> GeometryCollection<'a> for ClosedSurfaceMesh2d +where + T: Scalar, + Mesh2d: GeometryCollection<'a>, +{ + type Geometry = as GeometryCollection<'a>>::Geometry; + + fn num_geometries(&self) -> usize { + self.mesh.num_geometries() + } + + fn get_geometry(&'a self, index: usize) -> Option { + self.mesh.get_geometry(index) + } +} + +impl SignedDistance for ClosedSurfaceMesh2d +where + T: RealField, +{ + fn query_signed_distance(&self, point: &Point2) -> Option> { + let closest_edge = self.closest_edge(point)?; + + Some(SignedDistanceResult { + feature_id: closest_edge.edge_index, + point: closest_edge.edge_point, + signed_distance: closest_edge.signed_distance, + }) + } +} + +impl Polygon for ClosedSurfaceMesh2d +where + T: RealField, +{ + fn vertices(&self) -> &[Point] { + self.mesh().vertices() + } + + fn num_edges(&self) -> usize { + self.mesh().connectivity().len() + } + + fn get_edge(&self, index: usize) -> Option> { + self.mesh().get_cell(index) + } + + #[replace_float_literals(T::from_f64(literal).unwrap())] + fn pseudonormal_on_edge(&self, edge_index: usize, t: T) -> Option> { + let edge_conn = self.mesh().connectivity().get(edge_index)?; + let edge = edge_conn.cell(self.mesh().vertices())?; + + let [a_idx, b_idx] = edge_conn.0; + // If parameter suggests that the point is on a vertex, then use the vertex normal instead + let pseudo_normal = if t <= T::zero() { + self.vertex_normals[a_idx] + } else if t >= T::one() { + self.vertex_normals[b_idx] + } else { + edge.normal_dir().normalize() + }; + Some(pseudo_normal) + } +} diff --git a/fenris/src/mesh_convert.rs b/fenris/src/mesh_convert.rs new file mode 100644 index 0000000..3265732 --- /dev/null +++ b/fenris/src/mesh_convert.rs @@ -0,0 +1,730 @@ +use crate::connectivity::{ + Connectivity, ConnectivityMut, Hex20Connectivity, Hex27Connectivity, Hex8Connectivity, Quad4d2Connectivity, + Quad9d2Connectivity, Tet10Connectivity, Tet20Connectivity, Tet4Connectivity, Tri3d2Connectivity, + Tri6d2Connectivity, +}; +use crate::element::{ElementConnectivity, FiniteElement}; +use crate::mesh::{Mesh, Mesh2d, Mesh3d, Tet4Mesh}; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, DimName, Point, Point2, Point3, RealField, Scalar, Vector2, Vector3, U3}; + +use crate::geometry::polymesh::{PolyMesh, PolyMesh3d}; +use crate::geometry::{OrientationTestResult, Triangle}; +use itertools::{izip, Itertools}; +use nested_vec::NestedVec; +use numeric_literals::replace_float_literals; +use rustc_hash::FxHashMap; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::error::Error; + +pub trait RefineFrom: ConnectivityMut +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, +{ + // TODO: Avoid allocating so much memory for small containers + /// Return a refined connectivity and populate the provided containers with + /// child indices and parent connectivity information. + /// + /// TODO: Explain how this works + fn refine( + connectivity: &Connectivity, + mesh_vertices: &[Point], + vertices: &mut Vec>, + child_indices: &mut Vec, + parents: &mut NestedVec, + ) -> Self; +} + +impl RefineFrom for Tet10Connectivity +where + T: RealField, + DefaultAllocator: Allocator, +{ + fn refine( + connectivity: &Tet4Connectivity, + mesh_vertices: &[Point3], + vertices: &mut Vec>, + child_indices: &mut Vec, + parents: &mut NestedVec, + ) -> Self { + let global_indices = connectivity.vertex_indices(); + + // Add vertex nodes + for v_idx in global_indices { + parents.push(&[*v_idx]); + child_indices.push(0); + vertices.push(mesh_vertices[*v_idx].clone()); + } + + let mut add_edge_node = |v_local_begin, v_local_end| { + let v_global_begin = global_indices[v_local_begin]; + let v_global_end = global_indices[v_local_end]; + parents.push(&[v_global_begin, v_global_end]); + child_indices.push(0); + let midpoint = mesh_vertices[v_global_begin] + .coords + .lerp(&mesh_vertices[v_global_end].coords, T::from_f64(0.5).unwrap()); + vertices.push(midpoint.into()); + }; + + add_edge_node(0, 1); + add_edge_node(1, 2); + add_edge_node(0, 2); + add_edge_node(0, 3); + add_edge_node(2, 3); + add_edge_node(1, 3); + + Tet10Connectivity([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + } +} + +#[replace_float_literals(T::from_f64(literal).unwrap())] +impl RefineFrom for Tet20Connectivity +where + T: RealField, + DefaultAllocator: Allocator, +{ + fn refine( + connectivity: &Tet4Connectivity, + mesh_vertices: &[Point3], + vertices: &mut Vec>, + child_indices: &mut Vec, + parents: &mut NestedVec, + ) -> Self { + let global_indices = connectivity.vertex_indices(); + + // Add vertex nodes + for v_idx in global_indices { + parents.push(&[*v_idx]); + child_indices.push(0); + vertices.push(mesh_vertices[*v_idx].clone()); + } + + let mut add_edge_nodes = |v_local_begin: usize, v_local_end: usize| { + let v_global_begin = global_indices[v_local_begin]; + let v_global_end = global_indices[v_local_end]; + + let a = &mesh_vertices[v_global_begin].coords; + let b = &mesh_vertices[v_global_end].coords; + + // Add the two child nodes of the cubic edge + parents.push(&[v_global_begin, v_global_end]); + parents.push(&[v_global_begin, v_global_end]); + + // We need to make sure that we associate the same edge points in different elements + // to the same global nodes. We do so by ordering the child indices along the edge + // from lowest global vertex index to highest global vertex index + if v_global_begin < v_global_end { + child_indices.push(0); + child_indices.push(1); + } else { + child_indices.push(1); + child_indices.push(0); + } + vertices.push(a.lerp(b, 1.0 / 3.0).into()); + vertices.push(a.lerp(b, 2.0 / 3.0).into()); + }; + + // Note: ordering here must match element node ordering perfectly + add_edge_nodes(0, 1); + add_edge_nodes(0, 2); + add_edge_nodes(0, 3); + add_edge_nodes(1, 2); + add_edge_nodes(1, 3); + add_edge_nodes(2, 3); + + let mut add_face_node = |a, b, c| { + // Convert local to global indices + let a = global_indices[a]; + let b = global_indices[b]; + let c = global_indices[c]; + parents.push(&[a, b, c]); + child_indices.push(0); + + // Add the barycenter/centroid of the face + let va = &mesh_vertices[a].coords; + let vb = &mesh_vertices[b].coords; + let vc = &mesh_vertices[c].coords; + vertices.push(((va + vb + vc) / 3.0).into()); + }; + + add_face_node(0, 1, 2); + add_face_node(0, 1, 3); + add_face_node(0, 2, 3); + add_face_node(1, 2, 3); + + Tet20Connectivity([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + } +} + +impl RefineFrom for Hex27Connectivity +where + T: RealField, + DefaultAllocator: Allocator, +{ + #[replace_float_literals(T::from_f64(literal).unwrap())] + fn refine( + connectivity: &Hex8Connectivity, + mesh_vertices: &[Point3], + vertices: &mut Vec>, + child_indices: &mut Vec, + parents: &mut NestedVec, + ) -> Self { + let global_indices = connectivity.vertex_indices(); + + // Add vertex nodes + for v_idx in global_indices { + parents.push(&[*v_idx]); + child_indices.push(0); + vertices.push(mesh_vertices[*v_idx].clone()); + } + + let mut add_edge_node = |v_local_begin, v_local_end| { + let v_global_begin = global_indices[v_local_begin]; + let v_global_end = global_indices[v_local_end]; + parents.push(&[v_global_begin, v_global_end]); + child_indices.push(0); + let midpoint = mesh_vertices[v_global_begin] + .coords + .lerp(&mesh_vertices[v_global_end].coords, 0.5); + vertices.push(midpoint.into()); + }; + + add_edge_node(0, 1); + add_edge_node(0, 3); + add_edge_node(0, 4); + add_edge_node(1, 2); + add_edge_node(1, 5); + add_edge_node(2, 3); + add_edge_node(2, 6); + add_edge_node(3, 7); + add_edge_node(4, 5); + add_edge_node(4, 7); + add_edge_node(5, 6); + add_edge_node(6, 7); + + // Use element to map reference coords to physical coords + let element = connectivity.element(mesh_vertices).unwrap(); + let mut add_face_node = |faces: &[usize], reference_vertex| { + let vertex = element.map_reference_coords(&reference_vertex); + child_indices.push(0); + vertices.push(vertex.into()); + + let mut array = parents.begin_array(); + for local_vertex_index in faces { + let global_vertex_index = global_indices[*local_vertex_index]; + array.push_single(global_vertex_index); + } + }; + + add_face_node(&[0, 1, 2, 3], Vector3::new(0.0, 0.0, -1.0)); + add_face_node(&[0, 1, 4, 5], Vector3::new(0.0, -1.0, 0.0)); + add_face_node(&[0, 3, 4, 7], Vector3::new(-1.0, 0.0, 0.0)); + add_face_node(&[1, 2, 5, 6], Vector3::new(1.0, 0.0, 0.0)); + add_face_node(&[2, 3, 6, 7], Vector3::new(0.0, 1.0, 0.0)); + add_face_node(&[4, 5, 6, 7], Vector3::new(0.0, 0.0, 1.0)); + + // Add center node + { + let reference_vertex = Vector3::new(0.0, 0.0, 0.0); + let vertex = element.map_reference_coords(&reference_vertex); + parents.push(global_indices); + child_indices.push(0); + vertices.push(vertex.into()); + } + + // TODO: This looks a bit silly + Hex27Connectivity([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, + ]) + } +} + +impl RefineFrom for Hex20Connectivity +where + T: RealField, + DefaultAllocator: Allocator, +{ + #[replace_float_literals(T::from_f64(literal).unwrap())] + fn refine( + connectivity: &Hex8Connectivity, + mesh_vertices: &[Point3], + vertices: &mut Vec>, + child_indices: &mut Vec, + parents: &mut NestedVec, + ) -> Self { + let global_indices = connectivity.vertex_indices(); + + // Add vertex nodes + for v_idx in global_indices { + parents.push(&[*v_idx]); + child_indices.push(0); + vertices.push(mesh_vertices[*v_idx].clone()); + } + + let mut add_edge_node = |v_local_begin, v_local_end| { + let v_global_begin = global_indices[v_local_begin]; + let v_global_end = global_indices[v_local_end]; + parents.push(&[v_global_begin, v_global_end]); + child_indices.push(0); + let midpoint = mesh_vertices[v_global_begin] + .coords + .lerp(&mesh_vertices[v_global_end].coords, 0.5); + vertices.push(midpoint.into()); + }; + + add_edge_node(0, 1); + add_edge_node(0, 3); + add_edge_node(0, 4); + add_edge_node(1, 2); + add_edge_node(1, 5); + add_edge_node(2, 3); + add_edge_node(2, 6); + add_edge_node(3, 7); + add_edge_node(4, 5); + add_edge_node(4, 7); + add_edge_node(5, 6); + add_edge_node(6, 7); + + // TODO: This looks a bit silly + Hex20Connectivity([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + } +} + +/// Stop-gap solution for generalizing mesh conversion. +/// +/// TODO: Remove this trait and use RefineFrom directly? Though this is not directly possible +/// due to conflicting impls. At least find a better name though! +pub trait FromTemp { + fn from(obj: T) -> Self; +} + +impl<'a, T, D, C, CNew> FromTemp<&'a Mesh> for Mesh +where + T: RealField, + D: DimName, + C: Connectivity, + CNew: RefineFrom, + DefaultAllocator: Allocator, +{ + fn from(mesh: &'a Mesh) -> Self { + // Workspaces are used to hold only per-connectivity information, + // which is later transformed to global data + let mut child_indices_workspace = Vec::new(); + let mut parents_workspace = NestedVec::new(); + let mut vertices_workspace = Vec::new(); + + // Global intermediate data + let mut child_indices = Vec::new(); + let mut parents = NestedVec::new(); + let mut intermediate_vertices = Vec::new(); + + let mut new_connectivity = Vec::new(); + + // First construct intermediate data. This means that we basically just lay out the + // data from each local connectivity refinement linearly. Then in the next step, + // we label vertices, making sure to label equivalent vertices with the same index, + // before finally reconstructing connectivity. + for conn in mesh.connectivity() { + child_indices_workspace.clear(); + parents_workspace.clear(); + vertices_workspace.clear(); + + let mut new_conn = CNew::refine( + conn, + mesh.vertices(), + &mut vertices_workspace, + &mut child_indices_workspace, + &mut parents_workspace, + ); + + assert_eq!( + child_indices_workspace.len(), + parents_workspace.len(), + "Invalid RefineFrom implementation: \ + Number of child indices and parent groups must be equal." + ); + assert_eq!( + child_indices_workspace.len(), + vertices_workspace.len(), + "Invalid RefineFrom implementation: \ + Number of child indices and vertices must be equal." + ); + + let intermediate_vertex_index_offset = child_indices.len(); + + intermediate_vertices.extend_from_slice(&vertices_workspace); + child_indices.extend_from_slice(&child_indices_workspace); + for parent_group in parents_workspace.iter() { + parents.push(parent_group); + // TODO: Sort here or in impl? Might as well do it here I guess? + parents.last_mut().unwrap().sort_unstable(); + } + + // Vertex indices are local with respect to the returned new vertices. + // By adding the offset, the connectivity holds global intermediate indices + for v_idx in new_conn.vertex_indices_mut() { + *v_idx += intermediate_vertex_index_offset; + } + + new_connectivity.push(new_conn); + } + + // Map (child index, parents) to final vertex index + let mut vertex_label_map = FxHashMap::default(); + let mut final_vertices = Vec::new(); + let mut next_available_vertex_index = 0; + + // Rewrite connectivity and label vertices, making sure to collect + // vertices with the same child index and parents under the same label + for conn in &mut new_connectivity { + for vertex_index in conn.vertex_indices_mut() { + let vertex_parents = parents.get(*vertex_index).unwrap(); + let vertex_child_index = child_indices[*vertex_index]; + + // TODO: Avoid double lookup + let key = (vertex_child_index, vertex_parents); + let final_vertex_index = if vertex_label_map.contains_key(&key) { + *vertex_label_map.get(&key).unwrap() + } else { + let vertex = intermediate_vertices[*vertex_index].clone(); + final_vertices.push(vertex); + + let final_index = next_available_vertex_index; + vertex_label_map.insert((vertex_child_index, vertex_parents), final_index); + next_available_vertex_index += 1; + final_index + }; + + *vertex_index = final_vertex_index; + } + } + + Mesh::from_vertices_and_connectivity(final_vertices, new_connectivity) + } +} + +impl From> for Mesh2d +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn from(initial_mesh: Mesh2d) -> Self { + let mut vertices = initial_mesh.vertices().to_vec(); + + // Holds edges on which vertices should be inserted + let mut edge_vertex_index_map = HashMap::new(); + + let mut new_connectivity = Vec::new(); + + for connectivity in initial_mesh.connectivity() { + // TODO: Find a nicer way to write this + let vertex_indices = connectivity.vertex_indices(); + let num_vertices = vertex_indices.len(); + let edges = vertex_indices + .iter() + .cycle() + .take(num_vertices + 1) + .tuple_windows(); + + // Add nodal vertices + let mut tri6_vertex_indices = [0usize; 6]; + for (i, index) in vertex_indices.iter().enumerate() { + tri6_vertex_indices[i] = *index; + } + + // Add vertices that are midpoints on edges + for ((a, b), vertex_index) in izip!(edges, &mut tri6_vertex_indices[3..]) { + // Sort the tuple so that edges are uniquely described + let edge = (a.min(b), a.max(b)); + + let index = edge_vertex_index_map.entry(edge).or_insert_with(|| { + let new_vertex_index = vertices.len(); + let (v_a, v_b) = (vertices[*a], vertices[*b]); + let midpoint = Point2::from((v_a.coords + v_b.coords) / 2.0); + vertices.push(midpoint); + new_vertex_index + }); + + *vertex_index = *index; + } + + // Finally add the new p-refined connectivity + new_connectivity.push(Tri6d2Connectivity(tri6_vertex_indices)); + } + + Mesh2d::from_vertices_and_connectivity(vertices, new_connectivity) + } +} + +impl From> for Mesh2d +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn from(initial_mesh: Mesh2d) -> Self { + let mut vertices = initial_mesh.vertices().to_vec(); + + // Holds edges on which vertices should be inserted + let mut edge_vertex_index_map = HashMap::new(); + + let mut new_connectivity = Vec::new(); + + for connectivity in initial_mesh.connectivity() { + // TODO: Find a nicer way to write this + let vertex_indices = connectivity.vertex_indices(); + let num_vertices = vertex_indices.len(); + let edges = vertex_indices + .iter() + .cycle() + .take(num_vertices + 1) + .tuple_windows(); + + // Add nodal vertices + let mut quad9_vertex_indices = [0usize; 9]; + for (i, index) in vertex_indices.iter().enumerate() { + quad9_vertex_indices[i] = *index; + } + + // Add vertices that are midpoints on edges + for ((a, b), vertex_index) in izip!(edges, &mut quad9_vertex_indices[4..]) { + // Sort the tuple so that edges are uniquely described + let edge = (a.min(b), a.max(b)); + + let index = edge_vertex_index_map.entry(edge).or_insert_with(|| { + let new_vertex_index = vertices.len(); + let (v_a, v_b) = (vertices[*a], vertices[*b]); + let midpoint = Point2::from((v_a.coords + v_b.coords) / 2.0); + vertices.push(midpoint); + new_vertex_index + }); + + *vertex_index = *index; + } + + // Add the midpoint of the cell + let element = connectivity.element(initial_mesh.vertices()).unwrap(); + let midpoint = Point2::from(element.map_reference_coords(&Vector2::zeros())); + quad9_vertex_indices[8] = vertices.len(); + vertices.push(midpoint); + + // Finally add the new p-refined connectivity + new_connectivity.push(Quad9d2Connectivity(quad9_vertex_indices)); + } + + Mesh2d::from_vertices_and_connectivity(vertices, new_connectivity) + } +} + +impl<'a, T> From<&'a Mesh3d> for Mesh3d +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn from(initial_mesh: &'a Mesh3d) -> Self { + >::from(initial_mesh) + } +} + +impl<'a, T> From<&'a Mesh3d> for Mesh3d +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn from(initial_mesh: &'a Mesh3d) -> Self { + >::from(initial_mesh) + } +} + +impl<'a, T> From<&'a Mesh3d> for Mesh3d +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] + fn from(initial_mesh: &'a Mesh3d) -> Self { + let mut new_connectivity = Vec::with_capacity(initial_mesh.connectivity().len()); + + for conn in initial_mesh.connectivity() { + let tet4_conn = Tet4Connectivity::from(conn); + new_connectivity.push(tet4_conn); + } + + let tet4_mesh = Mesh::from_vertices_and_connectivity(initial_mesh.vertices().to_vec(), new_connectivity); + tet4_mesh.keep_cells(&((0..tet4_mesh.connectivity().len()).collect::>())) + } +} + +impl<'a, T> From<&'a Mesh3d> for Mesh3d +where + T: RealField, +{ + fn from(initial_mesh: &'a Mesh3d) -> Self { + >::from(initial_mesh) + } +} + +impl<'a, T> From<&'a Mesh3d> for Mesh3d +where + T: RealField, +{ + fn from(initial_mesh: &'a Mesh3d) -> Self { + >::from(initial_mesh) + } +} + +impl<'a, T, D, C> From<&'a Mesh> for PolyMesh +where + T: Scalar, + D: DimName, + // TODO: Should somehow ensure that the face connectivity describes + // a counter-clockwise oriented polygon + C: Connectivity, + DefaultAllocator: Allocator, +{ + fn from(mesh: &'a Mesh) -> Self { + let vertices = mesh.vertices().to_vec(); + + // We are able to query each cell for its faces, but these faces are only described + // in terms of the vertices they connect. We consider any two faces with the same + // set of vertex indices to be the same face. We must moreover be careful to + // always preserve the order of the vertex indices for whatever face we choose + // to add, as they need to remain counter-clockwise. This also preserves the correct + // normal direction for boundary faces. + + let mut sorted_connectivities = NestedVec::new(); + for c in mesh.connectivity() { + for i in 0..c.num_faces() { + let face_conn = c.get_face_connectivity(i).unwrap(); + sorted_connectivities.push(face_conn.vertex_indices()); + sorted_connectivities.last_mut().unwrap().sort_unstable(); + } + } + + // Map face connectivities to face indices + let mut conn_map = HashMap::new(); + let mut faces = NestedVec::new(); + let mut cells = NestedVec::new(); + + let mut global_cell_face_index = 0; + for c in mesh.connectivity() { + let mut cell_array = cells.begin_array(); + for i in 0..c.num_faces() { + let face_conn = c.get_face_connectivity(i).unwrap(); + let sorted_conn = sorted_connectivities.get(global_cell_face_index).unwrap(); + + // TODO: Use entry to avoid double lookup + let face_idx = if let Some(face_idx) = conn_map.get(sorted_conn) { + *face_idx + } else { + let face_idx = faces.len(); + faces.push(face_conn.vertex_indices()); + conn_map.insert(sorted_conn, face_idx); + face_idx + }; + + cell_array.push_single(face_idx); + global_cell_face_index += 1; + } + } + + assert_eq!(cells.len(), mesh.connectivity().len()); + + PolyMesh::from_poly_data(vertices, faces, cells) + } +} + +impl<'a, T> TryFrom<&'a PolyMesh3d> for Tet4Mesh +where + T: RealField, +{ + // TODO: Proper Error type + type Error = Box; + + fn try_from(poly_mesh: &'a PolyMesh3d) -> Result { + let mut connectivity = Vec::new(); + + let get_face = |idx| { + poly_mesh + .get_face_connectivity(idx) + .expect("Logic error: Cell references non-existent face.") + }; + let get_vertex = |idx| { + poly_mesh + .vertices() + .get(idx) + .expect("Logic error: Face references non-existent vertex.") + }; + + for (cell_idx, cell) in poly_mesh.cell_connectivity_iter().enumerate() { + if cell.len() == 4 { + // We construct a tetrahedron by taking the first face + // (which becomes the base of the tetrahedron) and then + // finding the remaining apex vertex which is not referenced by the other faces, + // and finally connecting the base vertices to the apex. + let mut base_vertices = [0; 3]; + let base_face = get_face(cell[0]); + base_vertices.clone_from_slice(base_face); + + // Each remaining face should consist of two vertices from the base face, + // and one vertex not in the base face + let apex = get_face(cell[1]) + .iter() + .filter(|idx| !base_vertices.contains(idx)) + .next() + .ok_or_else(|| { + format!( + "Failure to convert: \ + Detected several faces with the same set of vertices in cell {}.", + cell_idx + ) + })?; + + for i in 1..4 { + let face = get_face(cell[i]); + let has_no_extra_vertices = face + .iter() + .all(|idx| base_vertices.contains(idx) || apex == idx); + + if !has_no_extra_vertices { + return Err(Box::from(format!( + "Failure to convert: The faces of cell {} do not form a \ + tetrahedral cell.", + cell_idx + ))); + } + } + + let base_tri = Triangle([ + *get_vertex(base_vertices[0]), + *get_vertex(base_vertices[1]), + *get_vertex(base_vertices[2]), + ]); + let apex_vertex = *get_vertex(*apex); + + // If the apex is "below" the triangle, flip the normal of the triangle + // by swapping some of its vertices + if base_tri.point_orientation(&apex_vertex) == OrientationTestResult::Negative { + base_vertices.swap(0, 1); + } + + // Now we know that the apex is "above" the triangle, in the sense that it is + // on the "non-negative" side of the triangle with respect to the normal. + // Then it only remains to connect the base to the apex. + let mut tet4_vertex_indices = [0; 4]; + tet4_vertex_indices[0..3].clone_from_slice(&base_vertices); + tet4_vertex_indices[3] = *apex; + connectivity.push(Tet4Connectivity(tet4_vertex_indices)); + } else { + return Err(Box::from("Failure to convert: Detected non-tetrahedral cell.")); + } + } + + Ok(Mesh::from_vertices_and_connectivity( + poly_mesh.vertices().to_vec(), + connectivity, + )) + } +} diff --git a/fenris/src/model.rs b/fenris/src/model.rs new file mode 100644 index 0000000..7c139e0 --- /dev/null +++ b/fenris/src/model.rs @@ -0,0 +1,366 @@ +use crate::allocators::ElementConnectivityAllocator; +use crate::assembly::color_nodes; +use crate::connectivity::{ + CellConnectivity, Connectivity, Quad4d2Connectivity, Quad9d2Connectivity, Tet4Connectivity, Tri3d2Connectivity, + Tri6d2Connectivity, +}; +use crate::element::{map_physical_coordinates, ElementConnectivity, ReferenceFiniteElement}; +use crate::geometry::{Distance, DistanceQuery, GeometryCollection}; +use crate::mesh::Mesh; +use crate::quadrature::QuadraturePair; +use crate::space::GeometricFiniteElementSpace; +use itertools::izip; +use nalgebra::allocator::Allocator; +use nalgebra::{DVector, DefaultAllocator, DimMin, DimName, Point, RealField, Scalar, VectorN, U1, U2, U3}; +use paradis::DisjointSubsets; +use serde::{Deserialize, Serialize}; + +/// A finite element model consisting of vertices (physical nodes) and physical elements +/// that are defined by their connectivity to the vertices. +/// +/// This generalizes the usual finite element bases, such as standard Lagrangian polynomial +/// finite elements on quads/hex/tri/tet meshes. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound( + serialize = "T: Serialize,\ + >::Buffer: Serialize,\ + Connectivity: Serialize", + deserialize = "T: Deserialize<'de>,\ + >::Buffer: Deserialize<'de>,\ + Connectivity: Deserialize<'de>" +))] +pub struct NodalModel +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, +{ + mesh: Mesh, + + mass_quadrature: Option>, + stiffness_quadrature: Option>, + elliptic_quadrature: Option>, + + // Colors for parallel assembly + colors: Vec, +} + +pub type NodalModel2d = NodalModel; +pub type NodalModel3d = NodalModel; + +impl NodalModel +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, +{ + pub fn mesh(&self) -> &Mesh { + &self.mesh + } + + pub fn connectivity(&self) -> &[C] { + self.mesh.connectivity() + } + + pub fn vertices(&self) -> &[Point] { + self.mesh.vertices() + } + + pub fn mass_quadrature(&self) -> Option<&QuadraturePair> { + self.mass_quadrature.as_ref() + } + + pub fn stiffness_quadrature(&self) -> Option<&QuadraturePair> { + self.stiffness_quadrature.as_ref() + } + + pub fn elliptic_quadrature(&self) -> Option<&QuadraturePair> { + self.elliptic_quadrature.as_ref() + } + + pub fn colors(&self) -> &[DisjointSubsets] { + &self.colors + } + + /// Constructs a new model from the given mesh and quadrature. + /// + /// The same quadrature is used for all quadrature kinds. + /// + /// TODO: Remove/deprecate this method. It is currently only here for legacy reasons. + pub fn from_mesh_and_quadrature(mesh: Mesh, quadrature: (Vec, Vec>)) -> Self + where + C: Connectivity, + { + let colors = color_nodes(mesh.connectivity()); + Self { + mesh, + mass_quadrature: Some(quadrature.clone()), + stiffness_quadrature: Some(quadrature.clone()), + elliptic_quadrature: Some(quadrature.clone()), + colors, + } + } + + /// Constructs a new model from the given mesh, without attaching any quadrature. + pub fn from_mesh(mesh: Mesh) -> Self + where + C: Connectivity, + { + let colors = color_nodes(mesh.connectivity()); + Self { + mesh, + mass_quadrature: None, + stiffness_quadrature: None, + elliptic_quadrature: None, + colors, + } + } + + pub fn with_mass_quadrature(self, mass_quadrature: QuadraturePair) -> Self { + Self { + mass_quadrature: Some(mass_quadrature), + ..self + } + } + + pub fn with_stiffness_quadrature(self, stiffness_quadrature: QuadraturePair) -> Self { + Self { + stiffness_quadrature: Some(stiffness_quadrature), + ..self + } + } + + pub fn with_elliptic_quadrature(self, elliptic_quadrature: QuadraturePair) -> Self { + Self { + elliptic_quadrature: Some(elliptic_quadrature), + ..self + } + } +} + +pub type Quad4Model = NodalModel2d; +pub type Tri3d2Model = NodalModel2d; +pub type Tri6d2Model = NodalModel2d; +pub type Quad9Model = NodalModel2d; +pub type Tet4Model = NodalModel3d; + +impl<'a, T, D, C> GeometryCollection<'a> for NodalModel +where + T: Scalar, + D: DimName, + C: CellConnectivity, + DefaultAllocator: Allocator, +{ + type Geometry = C::Cell; + + fn num_geometries(&self) -> usize { + self.connectivity().len() + } + + fn get_geometry(&'a self, index: usize) -> Option { + self.connectivity().get(index)?.cell(self.vertices()) + } +} + +impl<'a, T, D, C, QueryGeometry> DistanceQuery<'a, QueryGeometry> for NodalModel +where + T: RealField, + D: DimName, + C: CellConnectivity, + Mesh: DistanceQuery<'a, QueryGeometry>, + DefaultAllocator: Allocator, +{ + fn nearest(&'a self, query_geometry: &QueryGeometry) -> Option { + self.mesh.nearest(query_geometry) + } +} + +/// Interpolates solution variables onto a fixed set of interpolation points. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FiniteElementInterpolator { + // Store the highest node index in supported_nodes, so that we can + // guarantee that we don't go out of bounds during interpolation. + max_node_index: Option, + + // For a set of points X_I and solution variables u, a finite element interpolation can be written + // u_h(X_I) = sum_J N_J(X_I) * u_J, + // where N_J is the basis function associated with node J, u_J is the solution variable + // associated with node J (basis weight) and u_h is the interpolation solution. Since the basis + // functions have local support, it suffices to consider nodes J for which X_I lies in the + // support of N_J. + // While the above is essentially a matrix-vector multiplication, we want to work with + // low-dimensional point and vector types. Thus we implement a CSR-like custom format + // that lets us compactly represent the weights. + + // Offsets into the node support vector. supported_node_offsets[I] gives the + // index of the first node that is supported on X_I. The length of support_node_offsets is + // m + 1, where m is the number of interpolation points X_I. + // This way the number of supported bases for a given point I is given by + // count = supported_node_offsets[I + 1] - supported_node_offsets[I]. + supported_node_offsets: Vec, + + /// Stores the value N and index of the basis function of each supported node + node_values: Vec<(T, usize)>, +} + +impl FiniteElementInterpolator +where + T: RealField, +{ + pub fn interpolate(&self, u: &DVector) -> Vec> + where + SolutionDim: DimName, + DefaultAllocator: Allocator, + { + let num_sol_vectors = self.supported_node_offsets.len().saturating_sub(1); + let mut sol_vectors = vec![VectorN::zeros(); num_sol_vectors]; + self.interpolate_into(&mut sol_vectors, u); + sol_vectors + } + + // TODO: Take "arbitrary" u, not just DVector + pub fn interpolate_into(&self, result: &mut [VectorN], u: &DVector) + where + SolutionDim: DimName, + DefaultAllocator: Allocator, + { + assert_eq!( + result.len() + 1, + self.supported_node_offsets.len(), + "Number of interpolation points must match." + ); + assert!( + self.max_node_index.is_none() || SolutionDim::dim() * self.max_node_index.unwrap() < u.len(), + "Cannot reference degrees of freedom not present in solution variables" + ); + + for i in 0..result.len() { + let i_support_start = self.supported_node_offsets[i]; + let i_support_end = self.supported_node_offsets[i + 1]; + + result[i].fill(T::zero()); + + for (v, j) in &self.node_values[i_support_start..i_support_end] { + let u_j = u.fixed_slice::(SolutionDim::dim() * j, 0); + result[i] += u_j * v.clone(); + } + } + } +} + +impl FiniteElementInterpolator { + pub fn from_compressed_values(node_values: Vec<(T, usize)>, supported_node_offsets: Vec) -> Self { + assert!( + supported_node_offsets + .iter() + .all(|i| *i < node_values.len() + 1), + "Supported node offsets must be in bounds with respect to supported nodes." + ); + + Self { + max_node_index: node_values.iter().map(|(_, i)| i).max().cloned(), + node_values, + supported_node_offsets, + } + } +} + +impl FiniteElementInterpolator { + pub fn interpolate_space<'a, Space, D>( + mesh: &'a Space, + interpolation_points: &'a [Point], + ) -> Result> + where + T: RealField, + D: DimName + DimMin, + Space: GeometricFiniteElementSpace<'a, T> + DistanceQuery<'a, Point>, + Space::Connectivity: ElementConnectivity, + DefaultAllocator: ElementConnectivityAllocator, + { + let mut supported_node_offsets = Vec::new(); + let mut node_values = Vec::new(); + + for point in interpolation_points { + let point_node_support_begin = node_values.len(); + supported_node_offsets.push(point_node_support_begin); + + if mesh.num_connectivities() > 0 { + let element_idx = mesh + .nearest(point) + .expect("Logic error: Mesh should have non-zero number of cells/elements."); + let conn = mesh.get_connectivity(element_idx).unwrap(); + let element = mesh.get_element(element_idx).unwrap(); + + let xi = map_physical_coordinates(&element, point) + .map_err(|_| "Failed to map physical coordinates to reference coordinates.")?; + + let basis_values = element.evaluate_basis(&xi.coords); + for (index, v) in izip!(conn.vertex_indices(), basis_values.into_iter()) { + node_values.push((v.clone(), index.clone())); + } + } + } + + supported_node_offsets.push(node_values.len()); + assert_eq!(interpolation_points.len() + 1, supported_node_offsets.len()); + + Ok(FiniteElementInterpolator::from_compressed_values( + node_values, + supported_node_offsets, + )) + } +} + +impl NodalModel +where + T: RealField, + D: DimName + DimMin, + Connectivity: CellConnectivity + ElementConnectivity, + Connectivity::Cell: Distance>, + Mesh: for<'a> GeometricFiniteElementSpace<'a, T, Connectivity = Connectivity>, + DefaultAllocator: ElementConnectivityAllocator, +{ + /// Creates an interpolator that interpolates solution variables at the given + /// interpolation points. + /// + /// Returns an error if the elements can not be converted to convex polygons, + /// or if an interpolation point is outside of the computational domain, + /// or if mapping a physical coordinate to a reference coordinate for the given + /// element fails. + /// TODO: Return proper error differentiating the different failure cases. + pub fn make_interpolator( + &self, + interpolation_points: &[Point], + ) -> Result, Box> { + FiniteElementInterpolator::interpolate_space(&self.mesh, interpolation_points) + } +} + +pub trait MakeInterpolator +where + T: RealField, + D: DimName + DimMin, + DefaultAllocator: Allocator, +{ + fn make_interpolator( + &self, + interpolation_points: &[Point], + ) -> Result, Box>; +} + +impl MakeInterpolator for NodalModel +where + T: RealField, + D: DimName + DimMin, + Connectivity: CellConnectivity + ElementConnectivity, + Connectivity::Cell: Distance>, + Mesh: for<'a> GeometricFiniteElementSpace<'a, T, Connectivity = Connectivity>, + DefaultAllocator: ElementConnectivityAllocator, +{ + fn make_interpolator( + &self, + interpolation_points: &[Point], + ) -> Result, Box> { + self.make_interpolator(interpolation_points) + } +} diff --git a/fenris/src/proptest.rs b/fenris/src/proptest.rs new file mode 100644 index 0000000..e258001 --- /dev/null +++ b/fenris/src/proptest.rs @@ -0,0 +1,129 @@ +use proptest::prelude::*; + +use crate::element::Tet4Element; +use crate::geometry::Orientation::Counterclockwise; +use crate::geometry::{Orientation, Triangle, Triangle2d, Triangle3d}; +use nalgebra::{Point2, Point3}; + +pub fn point2() -> impl Strategy> { + // Pick a reasonably small range to pick coordinates from, + // otherwise we can easily get floating point numbers that are + // so ridiculously large as to break anything we might want to do with them + let range = -10.0..10.0; + [range.clone(), range.clone()].prop_map(|[x, y]| Point2::new(x, y)) +} + +pub fn point3() -> impl Strategy> { + // Pick a reasonably small range to pick coordinates from, + // otherwise we can easily get floating point numbers that are + // so ridiculously large as to break anything we might want to do with them + let range = -10.0..10.0; + [range.clone(), range.clone(), range.clone()].prop_map(|[x, y, z]| Point3::new(x, y, z)) +} + +#[derive(Debug, Clone)] +pub struct Triangle3dParams { + orientation: Orientation, +} + +impl Triangle3dParams { + pub fn with_orientation(self, orientation: Orientation) -> Self { + Self { orientation, ..self } + } +} + +impl Default for Triangle3dParams { + fn default() -> Self { + Self { + orientation: Counterclockwise, + } + } +} + +#[derive(Debug, Clone)] +pub struct Triangle2dParams { + orientation: Orientation, +} + +impl Triangle2dParams { + pub fn with_orientation(self, orientation: Orientation) -> Self { + Self { orientation, ..self } + } +} + +impl Default for Triangle2dParams { + fn default() -> Self { + Self { + orientation: Counterclockwise, + } + } +} + +impl Arbitrary for Triangle3d { + // TODO: Parameter for extents (i.e. bounding box or so) + type Parameters = Triangle3dParams; // TODO: Avoid boxing for performance...? + type Strategy = BoxedStrategy; + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + let points = [point3(), point3(), point3()]; + points + .prop_map(|points| Triangle(points)) + .prop_map(move |mut triangle| { + if triangle.orientation() != args.orientation { + triangle.swap_vertices(0, 1); + } + triangle + }) + .boxed() + } +} + +impl Arbitrary for Triangle2d { + // TODO: Parameter for extents (i.e. bounding box or so) + type Parameters = Triangle2dParams; // TODO: Avoid boxing for performance...? + type Strategy = BoxedStrategy; + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + let points = [point2(), point2(), point2()]; + points + .prop_map(|points| Triangle(points)) + .prop_map(move |mut triangle| { + if triangle.orientation() != args.orientation { + triangle.swap_vertices(0, 1); + } + triangle + }) + .boxed() + } +} + +impl Arbitrary for Tet4Element { + // TODO: Reasonable parameters? + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + any_with::>(Triangle3dParams::default().with_orientation(Counterclockwise)) + .prop_flat_map(|triangle| { + // To create an arbitrary tetrahedron element, we take a counter-clockwise oriented + // triangle, and pick a point somewhere on the "positive" side of the + // triangle plane. We do this by associating a parameter with each + // tangent vector defined by the sides of the triangle, + // plus a non-negative parameter that scales along the normal direction + let range = -10.0..10.0; + let tangent_params = [range.clone(), range.clone(), range.clone()]; + let normal_param = 0.0..=10.0; + (Just(triangle), tangent_params, normal_param) + }) + .prop_map(|(triangle, tangent_params, normal_param)| { + let mut tangent_pos = triangle.centroid(); + + for (side, param) in triangle.sides().iter().zip(&tangent_params) { + tangent_pos.coords += *param * side; + } + let coord = tangent_pos + normal_param * triangle.normal_dir().normalize(); + Tet4Element::from_vertices([triangle.0[0], triangle.0[1], triangle.0[2], coord]) + }) + .boxed() + } +} diff --git a/fenris/src/quadrature.rs b/fenris/src/quadrature.rs new file mode 100644 index 0000000..63ca3f9 --- /dev/null +++ b/fenris/src/quadrature.rs @@ -0,0 +1,1637 @@ +use nalgebra::{DefaultAllocator, DimName, RealField, Scalar, Vector2, Vector3, VectorN, U1, U2, U3}; +use std::ops::{Add, AddAssign, Deref, Mul}; + +use nalgebra::allocator::Allocator; +use num::Zero; +use numeric_literals::replace_float_literals; + +pub type QuadraturePair = (Vec, Vec>); +pub type QuadraturePair2d = QuadraturePair; +pub type QuadraturePair3d = QuadraturePair; + +pub trait Quadrature +where + T: Scalar, + D: DimName, + DefaultAllocator: Allocator, +{ + fn weights(&self) -> &[T]; + fn points(&self) -> &[VectorN]; + + /// Approximates the integral of the given function using this quadrature rule. + fn integrate(&self, f: Function) -> U + where + Function: Fn(&VectorN) -> U, + U: Zero + Mul + Add + AddAssign, + { + let mut integral = U::zero(); + for (w, p) in self.weights().iter().zip(self.points()) { + integral += f(p) * w.clone(); + } + integral + } +} + +/// Helper trait for 2D quadratures. +pub trait Quadrature2d: Quadrature +where + T: Scalar, +{ +} + +impl Quadrature2d for X +where + T: Scalar, + X: Quadrature, +{ +} + +impl Quadrature for (A, B) +where + T: Scalar, + D: DimName, + A: Deref, + B: Deref]>, + DefaultAllocator: Allocator, +{ + fn weights(&self) -> &[T] { + self.0.deref() + } + + fn points(&self) -> &[VectorN] { + self.1.deref() + } +} + +impl Quadrature for &X +where + T: Scalar, + D: DimName, + X: Quadrature, + DefaultAllocator: Allocator, +{ + fn weights(&self) -> &[T] { + X::weights(self) + } + + fn points(&self) -> &[VectorN] { + X::points(self) + } +} + +/// Returns a quadrature rule for Quads on the reference domain [-1, 1]^2 of strength 5, +/// meaning that it exactly integrates polynomials of total order 5 and less. +/// +/// TODO: Generalize to other types and quadrature rules! +#[allow(clippy::unreadable_literal)] +#[allow(clippy::excessive_precision)] +pub fn quad_quadrature_strength_5_f64() -> (Vec, Vec>) { + let mut w = Vec::new(); + let mut p = Vec::new(); + p.reserve(8); + w.reserve(8); + + // Quadrature rule obtained from + // Witherden & Vincent, 2015, + // "On the identification of symmetric quadrature rules for finite element methods" + + w.push(0.8163265306122448979591836734693877551); + p.push(Vector2::new(0.68313005106397322554806924536807013272, 0.0)); + w.push(0.8163265306122448979591836734693877551); + p.push(Vector2::new(0.0, 0.68313005106397322554806924536807013272)); + w.push(0.8163265306122448979591836734693877551); + p.push(Vector2::new(-0.68313005106397322554806924536807013272, 0.0)); + w.push(0.8163265306122448979591836734693877551); + p.push(Vector2::new(0.0, -0.68313005106397322554806924536807013272)); + w.push(0.1836734693877551020408163265306122449); + p.push(Vector2::new( + 0.8819171036881968635005385845464201419, + 0.8819171036881968635005385845464201419, + )); + w.push(0.1836734693877551020408163265306122449); + p.push(Vector2::new( + 0.8819171036881968635005385845464201419, + -0.8819171036881968635005385845464201419, + )); + w.push(0.1836734693877551020408163265306122449); + p.push(Vector2::new( + -0.8819171036881968635005385845464201419, + 0.8819171036881968635005385845464201419, + )); + w.push(0.1836734693877551020408163265306122449); + p.push(Vector2::new( + -0.8819171036881968635005385845464201419, + -0.8819171036881968635005385845464201419, + )); + + (w, p) +} + +/// Returns a quadrature rule for Quads on the reference domain [-1, 1]^2 of strength 5, +/// meaning that it exactly integrates polynomials of total order 5 and less. +/// +/// TODO: Generalize to other types and quadrature rules! +#[allow(clippy::unreadable_literal)] +#[allow(clippy::excessive_precision)] +#[replace_float_literals(T::from_f64(literal).expect("Literal must be representable by T"))] +pub fn quad_quadrature_strength_5() -> (Vec, Vec>) +where + T: RealField, +{ + let mut w = Vec::new(); + let mut p = Vec::new(); + p.reserve(8); + w.reserve(8); + + // Quadrature rule obtained from + // Witherden & Vincent, 2015, + // "On the identification of symmetric quadrature rules for finite element methods" + + w.push(0.8163265306122448979591836734693877551); + p.push(Vector2::new(0.68313005106397322554806924536807013272, 0.0)); + w.push(0.8163265306122448979591836734693877551); + p.push(Vector2::new(0.0, 0.68313005106397322554806924536807013272)); + w.push(0.8163265306122448979591836734693877551); + p.push(Vector2::new(-0.68313005106397322554806924536807013272, 0.0)); + w.push(0.8163265306122448979591836734693877551); + p.push(Vector2::new(0.0, -0.68313005106397322554806924536807013272)); + w.push(0.1836734693877551020408163265306122449); + p.push(Vector2::new( + 0.8819171036881968635005385845464201419, + 0.8819171036881968635005385845464201419, + )); + w.push(0.1836734693877551020408163265306122449); + p.push(Vector2::new( + 0.8819171036881968635005385845464201419, + -0.8819171036881968635005385845464201419, + )); + w.push(0.1836734693877551020408163265306122449); + p.push(Vector2::new( + -0.8819171036881968635005385845464201419, + 0.8819171036881968635005385845464201419, + )); + w.push(0.1836734693877551020408163265306122449); + p.push(Vector2::new( + -0.8819171036881968635005385845464201419, + -0.8819171036881968635005385845464201419, + )); + + (w, p) +} + +#[allow(clippy::unreadable_literal)] +#[allow(clippy::excessive_precision)] +pub fn tri_vertex_quadrature() -> (Vec, Vec>) +where + T: RealField, +{ + let x: Vec<_> = vec![-1.0, 1.0, -1.0] + .into_iter() + .map(|t| T::from_f64(t).unwrap()) + .collect(); + let y: Vec<_> = vec![-1.0, -1.0, 1.0] + .into_iter() + .map(|t| T::from_f64(t).unwrap()) + .collect(); + let w = vec![1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0] + .into_iter() + .map(|t| T::from_f64(t).unwrap()) + .collect(); + + let p = x + .into_iter() + .zip(y.into_iter()) + .map(|(x, y)| Vector2::new(x, y)) + .collect(); + + (w, p) +} + +#[allow(clippy::unreadable_literal)] +#[allow(clippy::excessive_precision)] +#[replace_float_literals(T::from_f64(literal).unwrap())] +pub fn tri_quadrature_strength_2() -> (Vec, Vec>) +where + T: RealField, +{ + let w = vec![ + 0.66666666666666666666666666666666666667, + 0.66666666666666666666666666666666666667, + 0.66666666666666666666666666666666666667, + ]; + + let x = vec![ + -0.66666666666666666666666666666666666667, + 0.33333333333333333333333333333333333333, + -0.66666666666666666666666666666666666667, + ]; + + let y = vec![ + 0.33333333333333333333333333333333333333, + -0.66666666666666666666666666666666666667, + -0.66666666666666666666666666666666666667, + ]; + + let p = x + .into_iter() + .zip(y.into_iter()) + .map(|(x, y)| Vector2::new(x, y)) + .collect(); + + (w, p) +} + +#[allow(clippy::unreadable_literal)] +#[allow(clippy::excessive_precision)] +pub fn tri_quadrature_strength_5_f64() -> (Vec, Vec>) { + let x = vec![ + -0.33333333333333333333333333333333333333, + -0.79742698535308732239802527616975234389, + 0.59485397070617464479605055233950468778, + -0.79742698535308732239802527616975234389, + -0.059715871789769820459117580973104798968, + -0.88056825642046035908176483805379040206, + -0.059715871789769820459117580973104798968, + ]; + let y = vec![ + -0.33333333333333333333333333333333333333, + 0.59485397070617464479605055233950468778, + -0.79742698535308732239802527616975234389, + -0.79742698535308732239802527616975234389, + -0.88056825642046035908176483805379040206, + -0.059715871789769820459117580973104798968, + -0.059715871789769820459117580973104798968, + ]; + let w = vec![ + 0.45, + 0.25187836108965430519136789100036266732, + 0.25187836108965430519136789100036266732, + 0.25187836108965430519136789100036266732, + 0.26478830557701236147529877566630399935, + 0.26478830557701236147529877566630399935, + 0.26478830557701236147529877566630399935, + ]; + + let p = x + .into_iter() + .zip(y.into_iter()) + .map(|(x, y)| Vector2::new(x, y)) + .collect(); + + (w, p) +} + +#[allow(clippy::unreadable_literal)] +#[allow(clippy::excessive_precision)] +pub fn tri_quadrature_strength_5() -> (Vec, Vec>) +where + T: RealField, +{ + // TODO: Extend numeric_literals so that it can be used with macros + // like vec!, assert! etc (see PR #1 for progress on this) + let x: Vec<_> = vec![ + -0.33333333333333333333333333333333333333, + -0.79742698535308732239802527616975234389, + 0.59485397070617464479605055233950468778, + -0.79742698535308732239802527616975234389, + -0.059715871789769820459117580973104798968, + -0.88056825642046035908176483805379040206, + -0.059715871789769820459117580973104798968, + ] + .into_iter() + .map(|t| T::from_f64(t).unwrap()) + .collect(); + let y: Vec<_> = vec![ + -0.33333333333333333333333333333333333333, + 0.59485397070617464479605055233950468778, + -0.79742698535308732239802527616975234389, + -0.79742698535308732239802527616975234389, + -0.88056825642046035908176483805379040206, + -0.059715871789769820459117580973104798968, + -0.059715871789769820459117580973104798968, + ] + .into_iter() + .map(|t| T::from_f64(t).unwrap()) + .collect(); + let w = vec![ + 0.45, + 0.25187836108965430519136789100036266732, + 0.25187836108965430519136789100036266732, + 0.25187836108965430519136789100036266732, + 0.26478830557701236147529877566630399935, + 0.26478830557701236147529877566630399935, + 0.26478830557701236147529877566630399935, + ] + .into_iter() + .map(|t| T::from_f64(t).unwrap()) + .collect(); + + let p = x + .into_iter() + .zip(y.into_iter()) + .map(|(x, y)| Vector2::new(x, y)) + .collect(); + + (w, p) +} + +#[allow(clippy::unreadable_literal)] +#[allow(clippy::excessive_precision)] +pub fn tri_quadrature_strength_11() -> (Vec, Vec>) +where + T: RealField, +{ + let x: Vec<_> = vec![ + -0.33333333333333333333333333333333333333, + -0.94302916477125618104036726638981901865, + 0.8860583295425123620807345327796380373, + -0.94302916477125618104036726638981901865, + -0.57956008659364348598387546330841477973, + 0.15912017318728697196775092661682955946, + -0.57956008659364348598387546330841477973, + -0.79472903457550713910176986023011167594, + 0.58945806915101427820353972046022335189, + -0.79472903457550713910176986023011167594, + -0.0082161980682181738701197619927680147278, + -0.98356760386356365225976047601446397054, + -0.0082161980682181738701197619927680147278, + -0.12306814647129561795332827431496109166, + -0.75386370705740876409334345137007781667, + -0.12306814647129561795332827431496109166, + -0.70135042269583522760854842424565295474, + 0.68669956732370632439798287279764986108, + -0.98534914462787109678943444855199690633, + 0.68669956732370632439798287279764986108, + -0.98534914462787109678943444855199690633, + -0.70135042269583522760854842424565295474, + -0.90797899966914008822008044741026548976, + 0.3288167483937283948070953526650315723, + -0.42083774872458830658701490525476608254, + 0.3288167483937283948070953526650315723, + -0.42083774872458830658701490525476608254, + -0.90797899966914008822008044741026548976, + ] + .into_iter() + .map(|t| T::from_f64(t).unwrap()) + .collect(); + let y: Vec<_> = vec![ + -0.33333333333333333333333333333333333333, + 0.8860583295425123620807345327796380373, + -0.94302916477125618104036726638981901865, + -0.94302916477125618104036726638981901865, + 0.15912017318728697196775092661682955946, + -0.57956008659364348598387546330841477973, + -0.57956008659364348598387546330841477973, + 0.58945806915101427820353972046022335189, + -0.79472903457550713910176986023011167594, + -0.79472903457550713910176986023011167594, + -0.98356760386356365225976047601446397054, + -0.0082161980682181738701197619927680147278, + -0.0082161980682181738701197619927680147278, + -0.75386370705740876409334345137007781667, + -0.12306814647129561795332827431496109166, + -0.12306814647129561795332827431496109166, + 0.68669956732370632439798287279764986108, + -0.70135042269583522760854842424565295474, + 0.68669956732370632439798287279764986108, + -0.98534914462787109678943444855199690633, + -0.70135042269583522760854842424565295474, + -0.98534914462787109678943444855199690633, + 0.3288167483937283948070953526650315723, + -0.90797899966914008822008044741026548976, + 0.3288167483937283948070953526650315723, + -0.42083774872458830658701490525476608254, + -0.90797899966914008822008044741026548976, + -0.42083774872458830658701490525476608254, + ] + .into_iter() + .map(|t| T::from_f64(t).unwrap()) + .collect(); + let w = vec![ + 0.17152235946444843427639825073065398895, + 0.020863741025789391746056984197324633703, + 0.020863741025789391746056984197324633703, + 0.020863741025789391746056984197324633703, + 0.14103136822343315668414771834585673963, + 0.14103136822343315668414771834585673963, + 0.14103136822343315668414771834585673963, + 0.077261518474038644953267609178802298293, + 0.077261518474038644953267609178802298293, + 0.077261518474038644953267609178802298293, + 0.03321254610917073915275167855011212173, + 0.03321254610917073915275167855011212173, + 0.03321254610917073915275167855011212173, + 0.13463230815893660239567441124657557177, + 0.13463230815893660239567441124657557177, + 0.13463230815893660239567441124657557177, + 0.020580579145906554986161254967959059438, + 0.020580579145906554986161254967959059438, + 0.020580579145906554986161254967959059438, + 0.020580579145906554986161254967959059438, + 0.020580579145906554986161254967959059438, + 0.020580579145906554986161254967959059438, + 0.080664953281001105168489835817596259842, + 0.080664953281001105168489835817596259842, + 0.080664953281001105168489835817596259842, + 0.080664953281001105168489835817596259842, + 0.080664953281001105168489835817596259842, + 0.080664953281001105168489835817596259842, + ] + .into_iter() + .map(|t| T::from_f64(t).unwrap()) + .collect(); + + let p = x + .into_iter() + .zip(y.into_iter()) + .map(|(x, y)| Vector2::new(x, y)) + .collect(); + + (w, p) +} + +#[allow(clippy::unreadable_literal)] +#[allow(clippy::excessive_precision)] +pub fn quad_quadrature_strength_11() -> (Vec, Vec>) +where + T: RealField, +{ + let x: Vec<_> = vec![ + 0.71461782966460591762942382404176367266, + 0.0, + -0.71461782966460591762942382404176367266, + 0.0, + 0.27365721017145961383557026431906649027, + 0.27365721017145961383557026431906649027, + -0.27365721017145961383557026431906649027, + -0.27365721017145961383557026431906649027, + 0.63660393221230104405852833198974404207, + 0.63660393221230104405852833198974404207, + -0.63660393221230104405852833198974404207, + -0.63660393221230104405852833198974404207, + 0.95163038878403345881049829363696371918, + 0.81556543368963841306389865463928759233, + 0.95163038878403345881049829363696371918, + -0.81556543368963841306389865463928759233, + -0.95163038878403345881049829363696371918, + 0.81556543368963841306389865463928759233, + -0.95163038878403345881049829363696371918, + -0.81556543368963841306389865463928759233, + 0.34620720004764544118747320724330043979, + 0.93556787148759108135480212161830515337, + 0.34620720004764544118747320724330043979, + -0.93556787148759108135480212161830515337, + -0.34620720004764544118747320724330043979, + 0.93556787148759108135480212161830515337, + -0.34620720004764544118747320724330043979, + -0.93556787148759108135480212161830515337, + ] + .into_iter() + .map(|t| T::from_f64(t).unwrap()) + .collect(); + + let y: Vec<_> = vec![ + 0.0, + 0.71461782966460591762942382404176367266, + 0.0, + -0.71461782966460591762942382404176367266, + 0.27365721017145961383557026431906649027, + -0.27365721017145961383557026431906649027, + 0.27365721017145961383557026431906649027, + -0.27365721017145961383557026431906649027, + 0.63660393221230104405852833198974404207, + -0.63660393221230104405852833198974404207, + 0.63660393221230104405852833198974404207, + -0.63660393221230104405852833198974404207, + 0.81556543368963841306389865463928759233, + 0.95163038878403345881049829363696371918, + -0.81556543368963841306389865463928759233, + 0.95163038878403345881049829363696371918, + 0.81556543368963841306389865463928759233, + -0.95163038878403345881049829363696371918, + -0.81556543368963841306389865463928759233, + -0.95163038878403345881049829363696371918, + 0.93556787148759108135480212161830515337, + 0.34620720004764544118747320724330043979, + -0.93556787148759108135480212161830515337, + 0.34620720004764544118747320724330043979, + 0.93556787148759108135480212161830515337, + -0.34620720004764544118747320724330043979, + -0.93556787148759108135480212161830515337, + -0.34620720004764544118747320724330043979, + ] + .into_iter() + .map(|t| T::from_f64(t).unwrap()) + .collect(); + + let w: Vec<_> = vec![ + 0.21740043986871200551566672515052291339, + 0.21740043986871200551566672515052291339, + 0.21740043986871200551566672515052291339, + 0.21740043986871200551566672515052291339, + 0.27727410298385108795028691796804462632, + 0.27727410298385108795028691796804462632, + 0.27727410298385108795028691796804462632, + 0.27727410298385108795028691796804462632, + 0.21393363787824810450280500660206134837, + 0.21393363787824810450280500660206134837, + 0.21393363787824810450280500660206134837, + 0.21393363787824810450280500660206134837, + 0.044074569114983092054304271154112061776, + 0.044074569114983092054304271154112061776, + 0.044074569114983092054304271154112061776, + 0.044074569114983092054304271154112061776, + 0.044074569114983092054304271154112061776, + 0.044074569114983092054304271154112061776, + 0.044074569114983092054304271154112061776, + 0.044074569114983092054304271154112061776, + 0.10162134051961130896131640398557349419, + 0.10162134051961130896131640398557349419, + 0.10162134051961130896131640398557349419, + 0.10162134051961130896131640398557349419, + 0.10162134051961130896131640398557349419, + 0.10162134051961130896131640398557349419, + 0.10162134051961130896131640398557349419, + 0.10162134051961130896131640398557349419, + ] + .into_iter() + .map(|t| T::from_f64(t).unwrap()) + .collect(); + + let p = x + .into_iter() + .zip(y.into_iter()) + .map(|(x, y)| Vector2::new(x, y)) + .collect(); + + (w, p) +} + +#[allow(clippy::unreadable_literal)] +#[allow(clippy::excessive_precision)] +#[replace_float_literals(T::from_f64(literal).unwrap())] +pub fn tet_quadrature_strength_1() -> (Vec, Vec>) +where + T: RealField, +{ + let w: Vec = vec![(4.0 / 3.0)]; + let x = vec![-0.5]; + let y = vec![-0.5]; + let z = vec![-0.5]; + + let p = x + .into_iter() + .zip(y.into_iter()) + .zip(z.into_iter()) + .map(|((x, y), z)| Vector3::new(x, y, z)) + .collect(); + + (w, p) +} + +#[allow(clippy::unreadable_literal)] +#[allow(clippy::excessive_precision)] +#[replace_float_literals(T::from_f64(literal).unwrap())] +pub fn tet_quadrature_strength_2() -> (Vec, Vec>) +where + T: RealField, +{ + let w: Vec = vec![ + 0.33333333333333333333333333333333333333, + 0.33333333333333333333333333333333333333, + 0.33333333333333333333333333333333333333, + 0.33333333333333333333333333333333333333, + ]; + let x = vec![ + -0.72360679774997896964091736687312762354, + -0.72360679774997896964091736687312762354, + 0.17082039324993690892275210061938287063, + -0.72360679774997896964091736687312762354, + ]; + let y = vec![ + -0.72360679774997896964091736687312762354, + 0.17082039324993690892275210061938287063, + -0.72360679774997896964091736687312762354, + -0.72360679774997896964091736687312762354, + ]; + let z = vec![ + 0.17082039324993690892275210061938287063, + -0.72360679774997896964091736687312762354, + -0.72360679774997896964091736687312762354, + -0.72360679774997896964091736687312762354, + ]; + + let p = x + .into_iter() + .zip(y.into_iter()) + .zip(z.into_iter()) + .map(|((x, y), z)| Vector3::new(x, y, z)) + .collect(); + + (w, p) +} + +#[allow(clippy::unreadable_literal)] +#[allow(clippy::excessive_precision)] +#[replace_float_literals(T::from_f64(literal).unwrap())] +pub fn tet_quadrature_strength_3() -> (Vec, Vec>) +where + T: RealField, +{ + let w: Vec = vec![ + 0.18162379004944980942342872025562069427, + 0.18162379004944980942342872025562069427, + 0.18162379004944980942342872025562069427, + 0.18162379004944980942342872025562069427, + 0.15170954328388352390990461307771263906, + 0.15170954328388352390990461307771263906, + 0.15170954328388352390990461307771263906, + 0.15170954328388352390990461307771263906, + ]; + + let x = vec![ + -0.34367339496723662642072827083693243093, + -0.34367339496723662642072827083693243093, + -0.9689798150982901207378151874892027072, + -0.34367339496723662642072827083693243093, + -0.78390550020314279176487322158837338344, + -0.78390550020314279176487322158837338344, + 0.35171650060942837529461966476512015033, + -0.78390550020314279176487322158837338344, + ]; + + let y = vec![ + -0.34367339496723662642072827083693243093, + -0.9689798150982901207378151874892027072, + -0.34367339496723662642072827083693243093, + -0.34367339496723662642072827083693243093, + -0.78390550020314279176487322158837338344, + 0.35171650060942837529461966476512015033, + -0.78390550020314279176487322158837338344, + -0.78390550020314279176487322158837338344, + ]; + + let z = vec![ + -0.9689798150982901207378151874892027072, + -0.34367339496723662642072827083693243093, + -0.34367339496723662642072827083693243093, + -0.34367339496723662642072827083693243093, + 0.35171650060942837529461966476512015033, + -0.78390550020314279176487322158837338344, + -0.78390550020314279176487322158837338344, + -0.78390550020314279176487322158837338344, + ]; + + let p = x + .into_iter() + .zip(y.into_iter()) + .zip(z.into_iter()) + .map(|((x, y), z)| Vector3::new(x, y, z)) + .collect(); + + (w, p) +} + +#[allow(clippy::unreadable_literal)] +#[allow(clippy::excessive_precision)] +#[replace_float_literals(T::from_f64(literal).unwrap())] +pub fn tet_quadrature_strength_5() -> (Vec, Vec>) +where + T: RealField, +{ + let w: Vec = vec![ + 0.15025056762402113439891420311104844508, + 0.15025056762402113439891420311104844508, + 0.15025056762402113439891420311104844508, + 0.15025056762402113439891420311104844508, + 0.097990724155149266058280273981770004697, + 0.097990724155149266058280273981770004697, + 0.097990724155149266058280273981770004697, + 0.097990724155149266058280273981770004697, + 0.05672802770277528858409257082700992237, + 0.05672802770277528858409257082700992237, + 0.05672802770277528858409257082700992237, + 0.05672802770277528858409257082700992237, + 0.05672802770277528858409257082700992237, + 0.05672802770277528858409257082700992237, + ]; + + let x = vec![ + -0.37822816147339878040530853247308433401, + -0.37822816147339878040530853247308433401, + -0.86531551557980365878407440258074699796, + -0.37822816147339878040530853247308433401, + -0.81452949937821754719535217252593878951, + -0.81452949937821754719535217252593878951, + 0.44358849813465264158605651757781636853, + -0.81452949937821754719535217252593878951, + -0.90899259174870070101623894744132112187, + -0.091007408251299298983761052558678878131, + -0.90899259174870070101623894744132112187, + -0.90899259174870070101623894744132112187, + -0.091007408251299298983761052558678878131, + -0.091007408251299298983761052558678878131, + ]; + + let y = vec![ + -0.37822816147339878040530853247308433401, + -0.86531551557980365878407440258074699796, + -0.37822816147339878040530853247308433401, + -0.37822816147339878040530853247308433401, + -0.81452949937821754719535217252593878951, + 0.44358849813465264158605651757781636853, + -0.81452949937821754719535217252593878951, + -0.81452949937821754719535217252593878951, + -0.091007408251299298983761052558678878131, + -0.90899259174870070101623894744132112187, + -0.90899259174870070101623894744132112187, + -0.091007408251299298983761052558678878131, + -0.90899259174870070101623894744132112187, + -0.091007408251299298983761052558678878131, + ]; + + let z = vec![ + -0.86531551557980365878407440258074699796, + -0.37822816147339878040530853247308433401, + -0.37822816147339878040530853247308433401, + -0.37822816147339878040530853247308433401, + 0.44358849813465264158605651757781636853, + -0.81452949937821754719535217252593878951, + -0.81452949937821754719535217252593878951, + -0.81452949937821754719535217252593878951, + -0.091007408251299298983761052558678878131, + -0.091007408251299298983761052558678878131, + -0.091007408251299298983761052558678878131, + -0.90899259174870070101623894744132112187, + -0.90899259174870070101623894744132112187, + -0.90899259174870070101623894744132112187, + ]; + + let p = x + .into_iter() + .zip(y.into_iter()) + .zip(z.into_iter()) + .map(|((x, y), z)| Vector3::new(x, y, z)) + .collect(); + + (w, p) +} + +#[allow(clippy::unreadable_literal)] +#[allow(clippy::excessive_precision)] +#[replace_float_literals(T::from_f64(literal).unwrap())] +pub fn tet_quadrature_strength_10() -> (Vec, Vec>) +where + T: RealField, +{ + let w = vec![ + 0.063199698074694317846318428237401466134, + 0.035916079989691599737018881339842778615, + 0.035916079989691599737018881339842778615, + 0.035916079989691599737018881339842778615, + 0.035916079989691599737018881339842778615, + 0.013158879622391177646076980573564101693, + 0.013158879622391177646076980573564101693, + 0.013158879622391177646076980573564101693, + 0.013158879622391177646076980573564101693, + 0.015191841626926975498161246507619114195, + 0.015191841626926975498161246507619114195, + 0.015191841626926975498161246507619114195, + 0.015191841626926975498161246507619114195, + 0.015191841626926975498161246507619114195, + 0.015191841626926975498161246507619114195, + 0.015191841626926975498161246507619114195, + 0.015191841626926975498161246507619114195, + 0.015191841626926975498161246507619114195, + 0.015191841626926975498161246507619114195, + 0.015191841626926975498161246507619114195, + 0.015191841626926975498161246507619114195, + 0.00048259245911900483231983784641134908616, + 0.00048259245911900483231983784641134908616, + 0.00048259245911900483231983784641134908616, + 0.00048259245911900483231983784641134908616, + 0.00048259245911900483231983784641134908616, + 0.00048259245911900483231983784641134908616, + 0.00048259245911900483231983784641134908616, + 0.00048259245911900483231983784641134908616, + 0.00048259245911900483231983784641134908616, + 0.00048259245911900483231983784641134908616, + 0.00048259245911900483231983784641134908616, + 0.00048259245911900483231983784641134908616, + 0.034319642640608095038714683012872940214, + 0.034319642640608095038714683012872940214, + 0.034319642640608095038714683012872940214, + 0.034319642640608095038714683012872940214, + 0.034319642640608095038714683012872940214, + 0.034319642640608095038714683012872940214, + 0.034319642640608095038714683012872940214, + 0.034319642640608095038714683012872940214, + 0.034319642640608095038714683012872940214, + 0.034319642640608095038714683012872940214, + 0.034319642640608095038714683012872940214, + 0.034319642640608095038714683012872940214, + 0.013514495573007723718021960153355702928, + 0.013514495573007723718021960153355702928, + 0.013514495573007723718021960153355702928, + 0.013514495573007723718021960153355702928, + 0.013514495573007723718021960153355702928, + 0.013514495573007723718021960153355702928, + 0.013514495573007723718021960153355702928, + 0.013514495573007723718021960153355702928, + 0.013514495573007723718021960153355702928, + 0.013514495573007723718021960153355702928, + 0.013514495573007723718021960153355702928, + 0.013514495573007723718021960153355702928, + 0.0087681963693812055566076536006009347954, + 0.0087681963693812055566076536006009347954, + 0.0087681963693812055566076536006009347954, + 0.0087681963693812055566076536006009347954, + 0.0087681963693812055566076536006009347954, + 0.0087681963693812055566076536006009347954, + 0.0087681963693812055566076536006009347954, + 0.0087681963693812055566076536006009347954, + 0.0087681963693812055566076536006009347954, + 0.0087681963693812055566076536006009347954, + 0.0087681963693812055566076536006009347954, + 0.0087681963693812055566076536006009347954, + 0.017209381065149320852393906999331987612, + 0.017209381065149320852393906999331987612, + 0.017209381065149320852393906999331987612, + 0.017209381065149320852393906999331987612, + 0.017209381065149320852393906999331987612, + 0.017209381065149320852393906999331987612, + 0.017209381065149320852393906999331987612, + 0.017209381065149320852393906999331987612, + 0.017209381065149320852393906999331987612, + 0.017209381065149320852393906999331987612, + 0.017209381065149320852393906999331987612, + 0.017209381065149320852393906999331987612, + ]; + + let x = vec![ + -0.5, + -0.3754998626096227045403833626263450896, + -0.3754998626096227045403833626263450896, + -0.87350041217113188637884991212096473119, + -0.3754998626096227045403833626263450896, + -0.77138069228530769882525760469269910942, + -0.77138069228530769882525760469269910942, + 0.31414207685592309647577281407809732827, + -0.77138069228530769882525760469269910942, + -0.66902794876077789679101975111094717095, + -0.66902794876077789679101975111094717095, + -0.17913852156206901142420431149697662502, + -0.97269500811508408036057162589509957901, + -0.17913852156206901142420431149697662502, + -0.17913852156206901142420431149697662502, + -0.97269500811508408036057162589509957901, + -0.17913852156206901142420431149697662502, + -0.17913852156206901142420431149697662502, + -0.17913852156206901142420431149697662502, + -0.97269500811508408036057162589509957901, + -0.66902794876077789679101975111094717095, + 0.8859775346904097323952611738365015259, + 0.8859775346904097323952611738365015259, + -0.98772398235041850430481257350316929785, + -0.9105295699895727237856360268301629302, + -0.98772398235041850430481257350316929785, + -0.98772398235041850430481257350316929785, + -0.9105295699895727237856360268301629302, + -0.98772398235041850430481257350316929785, + -0.98772398235041850430481257350316929785, + -0.98772398235041850430481257350316929785, + -0.9105295699895727237856360268301629302, + 0.8859775346904097323952611738365015259, + -0.045619240191439298911787183406185558751, + -0.045619240191439298911787183406185558751, + -0.75789963770882114801220999680989894474, + -0.43858148439091840506379282297401655176, + -0.75789963770882114801220999680989894474, + -0.75789963770882114801220999680989894474, + -0.43858148439091840506379282297401655176, + -0.75789963770882114801220999680989894474, + -0.75789963770882114801220999680989894474, + -0.75789963770882114801220999680989894474, + -0.43858148439091840506379282297401655176, + -0.045619240191439298911787183406185558751, + 0.18851253896001405132314006877136059652, + 0.18851253896001405132314006877136059652, + -0.93444106356711465845055795933535161918, + -0.31963041182578473442202415010065735815, + -0.93444106356711465845055795933535161918, + -0.93444106356711465845055795933535161918, + -0.31963041182578473442202415010065735815, + -0.93444106356711465845055795933535161918, + -0.93444106356711465845055795933535161918, + -0.93444106356711465845055795933535161918, + -0.31963041182578473442202415010065735815, + 0.18851253896001405132314006877136059652, + 0.60235456931668878246228336815716196359, + 0.60235456931668878246228336815716196359, + -0.93502943687035390432897012004314760659, + -0.73229569557598097380434312807086675041, + -0.93502943687035390432897012004314760659, + -0.93502943687035390432897012004314760659, + -0.73229569557598097380434312807086675041, + -0.93502943687035390432897012004314760659, + -0.93502943687035390432897012004314760659, + -0.93502943687035390432897012004314760659, + -0.73229569557598097380434312807086675041, + 0.60235456931668878246228336815716196359, + 0.2561436909507320213865521444358193313, + 0.2561436909507320213865521444358193313, + -0.65004131563212195143010154694337920556, + -0.95606105968648811852634905054906092018, + -0.65004131563212195143010154694337920556, + -0.65004131563212195143010154694337920556, + -0.95606105968648811852634905054906092018, + -0.65004131563212195143010154694337920556, + -0.65004131563212195143010154694337920556, + -0.65004131563212195143010154694337920556, + -0.95606105968648811852634905054906092018, + 0.2561436909507320213865521444358193313, + ]; + + let y = vec![ + -0.5, + -0.3754998626096227045403833626263450896, + -0.87350041217113188637884991212096473119, + -0.3754998626096227045403833626263450896, + -0.3754998626096227045403833626263450896, + -0.77138069228530769882525760469269910942, + 0.31414207685592309647577281407809732827, + -0.77138069228530769882525760469269910942, + -0.77138069228530769882525760469269910942, + -0.17913852156206901142420431149697662502, + -0.17913852156206901142420431149697662502, + -0.17913852156206901142420431149697662502, + -0.66902794876077789679101975111094717095, + -0.97269500811508408036057162589509957901, + -0.66902794876077789679101975111094717095, + -0.17913852156206901142420431149697662502, + -0.97269500811508408036057162589509957901, + -0.17913852156206901142420431149697662502, + -0.66902794876077789679101975111094717095, + -0.17913852156206901142420431149697662502, + -0.97269500811508408036057162589509957901, + -0.98772398235041850430481257350316929785, + -0.98772398235041850430481257350316929785, + -0.98772398235041850430481257350316929785, + 0.8859775346904097323952611738365015259, + -0.9105295699895727237856360268301629302, + 0.8859775346904097323952611738365015259, + -0.98772398235041850430481257350316929785, + -0.9105295699895727237856360268301629302, + -0.98772398235041850430481257350316929785, + 0.8859775346904097323952611738365015259, + -0.98772398235041850430481257350316929785, + -0.9105295699895727237856360268301629302, + -0.75789963770882114801220999680989894474, + -0.75789963770882114801220999680989894474, + -0.75789963770882114801220999680989894474, + -0.045619240191439298911787183406185558751, + -0.43858148439091840506379282297401655176, + -0.045619240191439298911787183406185558751, + -0.75789963770882114801220999680989894474, + -0.43858148439091840506379282297401655176, + -0.75789963770882114801220999680989894474, + -0.045619240191439298911787183406185558751, + -0.75789963770882114801220999680989894474, + -0.43858148439091840506379282297401655176, + -0.93444106356711465845055795933535161918, + -0.93444106356711465845055795933535161918, + -0.93444106356711465845055795933535161918, + 0.18851253896001405132314006877136059652, + -0.31963041182578473442202415010065735815, + 0.18851253896001405132314006877136059652, + -0.93444106356711465845055795933535161918, + -0.31963041182578473442202415010065735815, + -0.93444106356711465845055795933535161918, + 0.18851253896001405132314006877136059652, + -0.93444106356711465845055795933535161918, + -0.31963041182578473442202415010065735815, + -0.93502943687035390432897012004314760659, + -0.93502943687035390432897012004314760659, + -0.93502943687035390432897012004314760659, + 0.60235456931668878246228336815716196359, + -0.73229569557598097380434312807086675041, + 0.60235456931668878246228336815716196359, + -0.93502943687035390432897012004314760659, + -0.73229569557598097380434312807086675041, + -0.93502943687035390432897012004314760659, + 0.60235456931668878246228336815716196359, + -0.93502943687035390432897012004314760659, + -0.73229569557598097380434312807086675041, + -0.65004131563212195143010154694337920556, + -0.65004131563212195143010154694337920556, + -0.65004131563212195143010154694337920556, + 0.2561436909507320213865521444358193313, + -0.95606105968648811852634905054906092018, + 0.2561436909507320213865521444358193313, + -0.65004131563212195143010154694337920556, + -0.95606105968648811852634905054906092018, + -0.65004131563212195143010154694337920556, + 0.2561436909507320213865521444358193313, + -0.65004131563212195143010154694337920556, + -0.95606105968648811852634905054906092018, + ]; + + let z = vec![ + -0.5, + -0.87350041217113188637884991212096473119, + -0.3754998626096227045403833626263450896, + -0.3754998626096227045403833626263450896, + -0.3754998626096227045403833626263450896, + 0.31414207685592309647577281407809732827, + -0.77138069228530769882525760469269910942, + -0.77138069228530769882525760469269910942, + -0.77138069228530769882525760469269910942, + -0.97269500811508408036057162589509957901, + -0.17913852156206901142420431149697662502, + -0.66902794876077789679101975111094717095, + -0.17913852156206901142420431149697662502, + -0.66902794876077789679101975111094717095, + -0.17913852156206901142420431149697662502, + -0.66902794876077789679101975111094717095, + -0.17913852156206901142420431149697662502, + -0.97269500811508408036057162589509957901, + -0.97269500811508408036057162589509957901, + -0.17913852156206901142420431149697662502, + -0.17913852156206901142420431149697662502, + -0.9105295699895727237856360268301629302, + -0.98772398235041850430481257350316929785, + 0.8859775346904097323952611738365015259, + -0.98772398235041850430481257350316929785, + 0.8859775346904097323952611738365015259, + -0.98772398235041850430481257350316929785, + 0.8859775346904097323952611738365015259, + -0.98772398235041850430481257350316929785, + -0.9105295699895727237856360268301629302, + -0.9105295699895727237856360268301629302, + -0.98772398235041850430481257350316929785, + -0.98772398235041850430481257350316929785, + -0.43858148439091840506379282297401655176, + -0.75789963770882114801220999680989894474, + -0.045619240191439298911787183406185558751, + -0.75789963770882114801220999680989894474, + -0.045619240191439298911787183406185558751, + -0.75789963770882114801220999680989894474, + -0.045619240191439298911787183406185558751, + -0.75789963770882114801220999680989894474, + -0.43858148439091840506379282297401655176, + -0.43858148439091840506379282297401655176, + -0.75789963770882114801220999680989894474, + -0.75789963770882114801220999680989894474, + -0.31963041182578473442202415010065735815, + -0.93444106356711465845055795933535161918, + 0.18851253896001405132314006877136059652, + -0.93444106356711465845055795933535161918, + 0.18851253896001405132314006877136059652, + -0.93444106356711465845055795933535161918, + 0.18851253896001405132314006877136059652, + -0.93444106356711465845055795933535161918, + -0.31963041182578473442202415010065735815, + -0.31963041182578473442202415010065735815, + -0.93444106356711465845055795933535161918, + -0.93444106356711465845055795933535161918, + -0.73229569557598097380434312807086675041, + -0.93502943687035390432897012004314760659, + 0.60235456931668878246228336815716196359, + -0.93502943687035390432897012004314760659, + 0.60235456931668878246228336815716196359, + -0.93502943687035390432897012004314760659, + 0.60235456931668878246228336815716196359, + -0.93502943687035390432897012004314760659, + -0.73229569557598097380434312807086675041, + -0.73229569557598097380434312807086675041, + -0.93502943687035390432897012004314760659, + -0.93502943687035390432897012004314760659, + -0.95606105968648811852634905054906092018, + -0.65004131563212195143010154694337920556, + 0.2561436909507320213865521444358193313, + -0.65004131563212195143010154694337920556, + 0.2561436909507320213865521444358193313, + -0.65004131563212195143010154694337920556, + 0.2561436909507320213865521444358193313, + -0.65004131563212195143010154694337920556, + -0.95606105968648811852634905054906092018, + -0.95606105968648811852634905054906092018, + -0.65004131563212195143010154694337920556, + -0.65004131563212195143010154694337920556, + ]; + + let p = x + .into_iter() + .zip(y.into_iter()) + .zip(z.into_iter()) + .map(|((x, y), z)| Vector3::new(x, y, z)) + .collect(); + + (w, p) +} + +#[allow(clippy::unreadable_literal)] +#[allow(clippy::excessive_precision)] +#[replace_float_literals(T::from_f64(literal).unwrap())] +pub fn hex_quadrature_strength_3() -> (Vec, Vec>) +where + T: RealField, +{ + let w = vec![ + 1.3333333333333333333333333333333333333, + 1.3333333333333333333333333333333333333, + 1.3333333333333333333333333333333333333, + 1.3333333333333333333333333333333333333, + 1.3333333333333333333333333333333333333, + 1.3333333333333333333333333333333333333, + ]; + + let x = vec![-1.0, 0.0, 0.0, 0.0, 1.0, 0.0]; + + let y = vec![0.0, 0.0, 1.0, 0.0, 0.0, -1.0]; + + let z = vec![0.0, 1.0, 0.0, -1.0, 0.0, 0.0]; + + let p = x + .into_iter() + .zip(y.into_iter()) + .zip(z.into_iter()) + .map(|((x, y), z)| Vector3::new(x, y, z)) + .collect(); + + (w, p) +} + +#[allow(clippy::unreadable_literal)] +#[allow(clippy::excessive_precision)] +#[replace_float_literals(T::from_f64(literal).unwrap())] +pub fn hex_quadrature_strength_5() -> (Vec, Vec>) +where + T: RealField, +{ + let w = vec![ + 0.88642659279778393351800554016620498615, + 0.88642659279778393351800554016620498615, + 0.88642659279778393351800554016620498615, + 0.88642659279778393351800554016620498615, + 0.88642659279778393351800554016620498615, + 0.88642659279778393351800554016620498615, + 0.33518005540166204986149584487534626039, + 0.33518005540166204986149584487534626039, + 0.33518005540166204986149584487534626039, + 0.33518005540166204986149584487534626039, + 0.33518005540166204986149584487534626039, + 0.33518005540166204986149584487534626039, + 0.33518005540166204986149584487534626039, + 0.33518005540166204986149584487534626039, + ]; + + let x = vec![ + -0.79582242575422146326454882047613584616, + 0.0, + 0.0, + 0.0, + 0.79582242575422146326454882047613584616, + 0.0, + 0.75878691063932814626903427811226742764, + -0.75878691063932814626903427811226742764, + -0.75878691063932814626903427811226742764, + -0.75878691063932814626903427811226742764, + -0.75878691063932814626903427811226742764, + 0.75878691063932814626903427811226742764, + 0.75878691063932814626903427811226742764, + 0.75878691063932814626903427811226742764, + ]; + + let y = vec![ + 0.0, + 0.0, + 0.79582242575422146326454882047613584616, + 0.0, + 0.0, + -0.79582242575422146326454882047613584616, + -0.75878691063932814626903427811226742764, + 0.75878691063932814626903427811226742764, + 0.75878691063932814626903427811226742764, + -0.75878691063932814626903427811226742764, + -0.75878691063932814626903427811226742764, + 0.75878691063932814626903427811226742764, + 0.75878691063932814626903427811226742764, + -0.75878691063932814626903427811226742764, + ]; + + let z = vec![ + 0.0, + 0.79582242575422146326454882047613584616, + 0.0, + -0.79582242575422146326454882047613584616, + 0.0, + 0.0, + -0.75878691063932814626903427811226742764, + 0.75878691063932814626903427811226742764, + -0.75878691063932814626903427811226742764, + -0.75878691063932814626903427811226742764, + 0.75878691063932814626903427811226742764, + -0.75878691063932814626903427811226742764, + 0.75878691063932814626903427811226742764, + 0.75878691063932814626903427811226742764, + ]; + + let p = x + .into_iter() + .zip(y.into_iter()) + .zip(z.into_iter()) + .map(|((x, y), z)| Vector3::new(x, y, z)) + .collect(); + + (w, p) +} + +#[allow(clippy::unreadable_literal)] +#[allow(clippy::excessive_precision)] +#[replace_float_literals(T::from_f64(literal).unwrap())] +pub fn hex_quadrature_strength_11() -> (Vec, Vec>) +where + T: RealField, +{ + let w = vec![ + 0.2024770736128001905853371309670196589, + 0.2024770736128001905853371309670196589, + 0.2024770736128001905853371309670196589, + 0.2024770736128001905853371309670196589, + 0.2024770736128001905853371309670196589, + 0.2024770736128001905853371309670196589, + 0.11753834795645628038993180401068212711, + 0.11753834795645628038993180401068212711, + 0.11753834795645628038993180401068212711, + 0.11753834795645628038993180401068212711, + 0.11753834795645628038993180401068212711, + 0.11753834795645628038993180401068212711, + 0.11753834795645628038993180401068212711, + 0.11753834795645628038993180401068212711, + 0.044643912078829241641001154282130043664, + 0.044643912078829241641001154282130043664, + 0.044643912078829241641001154282130043664, + 0.044643912078829241641001154282130043664, + 0.044643912078829241641001154282130043664, + 0.044643912078829241641001154282130043664, + 0.044643912078829241641001154282130043664, + 0.044643912078829241641001154282130043664, + 0.21599204525496912931346666638444131361, + 0.21599204525496912931346666638444131361, + 0.21599204525496912931346666638444131361, + 0.21599204525496912931346666638444131361, + 0.21599204525496912931346666638444131361, + 0.21599204525496912931346666638444131361, + 0.21599204525496912931346666638444131361, + 0.21599204525496912931346666638444131361, + 0.14519934586011569829250580079425982305, + 0.14519934586011569829250580079425982305, + 0.14519934586011569829250580079425982305, + 0.14519934586011569829250580079425982305, + 0.14519934586011569829250580079425982305, + 0.14519934586011569829250580079425982305, + 0.14519934586011569829250580079425982305, + 0.14519934586011569829250580079425982305, + 0.14519934586011569829250580079425982305, + 0.14519934586011569829250580079425982305, + 0.14519934586011569829250580079425982305, + 0.14519934586011569829250580079425982305, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.061441994097835335202750044633046200824, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + 0.022614296138821884223196230668984478131, + ]; + + let x = vec![ + -0.81261433409962649639237559737974432611, + 0.0, + 0.0, + 0.0, + 0.81261433409962649639237559737974432611, + 0.0, + 0.60167526419826270163441300578531749659, + -0.60167526419826270163441300578531749659, + -0.60167526419826270163441300578531749659, + -0.60167526419826270163441300578531749659, + -0.60167526419826270163441300578531749659, + 0.60167526419826270163441300578531749659, + 0.60167526419826270163441300578531749659, + 0.60167526419826270163441300578531749659, + 0.85545576101775998467509147069034657598, + -0.85545576101775998467509147069034657598, + -0.85545576101775998467509147069034657598, + -0.85545576101775998467509147069034657598, + -0.85545576101775998467509147069034657598, + 0.85545576101775998467509147069034657598, + 0.85545576101775998467509147069034657598, + 0.85545576101775998467509147069034657598, + 0.31339340451605472104577323055795129941, + -0.31339340451605472104577323055795129941, + -0.31339340451605472104577323055795129941, + -0.31339340451605472104577323055795129941, + -0.31339340451605472104577323055795129941, + 0.31339340451605472104577323055795129941, + 0.31339340451605472104577323055795129941, + 0.31339340451605472104577323055795129941, + -0.73466828699700801734638476986754918792, + 0.73466828699700801734638476986754918792, + 0.0, + 0.73466828699700801734638476986754918792, + 0.73466828699700801734638476986754918792, + 0.0, + 0.0, + -0.73466828699700801734638476986754918792, + -0.73466828699700801734638476986754918792, + 0.73466828699700801734638476986754918792, + 0.0, + -0.73466828699700801734638476986754918792, + 0.45079993511450943037788434573026952398, + -0.45079993511450943037788434573026952398, + -0.45079993511450943037788434573026952398, + -0.96509966551271026293028182312534456821, + -0.45079993511450943037788434573026952398, + 0.96509966551271026293028182312534456821, + 0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + -0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + -0.96509966551271026293028182312534456821, + -0.45079993511450943037788434573026952398, + -0.45079993511450943037788434573026952398, + -0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + 0.96509966551271026293028182312534456821, + 0.45079993511450943037788434573026952398, + 0.96509966551271026293028182312534456821, + 0.96509966551271026293028182312534456821, + -0.96509966551271026293028182312534456821, + 0.45079993511450943037788434573026952398, + -0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + -0.96509966551271026293028182312534456821, + 0.94124485721060326391115015763113464139, + -0.94124485721060326391115015763113464139, + -0.94124485721060326391115015763113464139, + -0.35390281459663013491031287081289167626, + -0.94124485721060326391115015763113464139, + 0.35390281459663013491031287081289167626, + 0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + -0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + -0.35390281459663013491031287081289167626, + -0.94124485721060326391115015763113464139, + -0.94124485721060326391115015763113464139, + -0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + 0.35390281459663013491031287081289167626, + 0.94124485721060326391115015763113464139, + 0.35390281459663013491031287081289167626, + 0.35390281459663013491031287081289167626, + -0.35390281459663013491031287081289167626, + 0.94124485721060326391115015763113464139, + -0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + -0.35390281459663013491031287081289167626, + ]; + + let y = vec![ + 0.0, + 0.0, + 0.81261433409962649639237559737974432611, + 0.0, + 0.0, + -0.81261433409962649639237559737974432611, + -0.60167526419826270163441300578531749659, + 0.60167526419826270163441300578531749659, + 0.60167526419826270163441300578531749659, + -0.60167526419826270163441300578531749659, + -0.60167526419826270163441300578531749659, + 0.60167526419826270163441300578531749659, + 0.60167526419826270163441300578531749659, + -0.60167526419826270163441300578531749659, + -0.85545576101775998467509147069034657598, + 0.85545576101775998467509147069034657598, + 0.85545576101775998467509147069034657598, + -0.85545576101775998467509147069034657598, + -0.85545576101775998467509147069034657598, + 0.85545576101775998467509147069034657598, + 0.85545576101775998467509147069034657598, + -0.85545576101775998467509147069034657598, + -0.31339340451605472104577323055795129941, + 0.31339340451605472104577323055795129941, + 0.31339340451605472104577323055795129941, + -0.31339340451605472104577323055795129941, + -0.31339340451605472104577323055795129941, + 0.31339340451605472104577323055795129941, + 0.31339340451605472104577323055795129941, + -0.31339340451605472104577323055795129941, + -0.73466828699700801734638476986754918792, + 0.0, + 0.73466828699700801734638476986754918792, + 0.73466828699700801734638476986754918792, + 0.0, + -0.73466828699700801734638476986754918792, + -0.73466828699700801734638476986754918792, + 0.0, + 0.73466828699700801734638476986754918792, + -0.73466828699700801734638476986754918792, + 0.73466828699700801734638476986754918792, + 0.0, + 0.96509966551271026293028182312534456821, + -0.96509966551271026293028182312534456821, + 0.96509966551271026293028182312534456821, + -0.45079993511450943037788434573026952398, + -0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + -0.96509966551271026293028182312534456821, + -0.96509966551271026293028182312534456821, + -0.96509966551271026293028182312534456821, + 0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + -0.45079993511450943037788434573026952398, + -0.45079993511450943037788434573026952398, + -0.45079993511450943037788434573026952398, + 0.96509966551271026293028182312534456821, + 0.45079993511450943037788434573026952398, + -0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + 0.96509966551271026293028182312534456821, + -0.45079993511450943037788434573026952398, + -0.45079993511450943037788434573026952398, + 0.35390281459663013491031287081289167626, + -0.35390281459663013491031287081289167626, + 0.35390281459663013491031287081289167626, + -0.94124485721060326391115015763113464139, + -0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + -0.35390281459663013491031287081289167626, + -0.35390281459663013491031287081289167626, + -0.35390281459663013491031287081289167626, + 0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + -0.94124485721060326391115015763113464139, + -0.94124485721060326391115015763113464139, + -0.94124485721060326391115015763113464139, + 0.35390281459663013491031287081289167626, + 0.94124485721060326391115015763113464139, + -0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + 0.35390281459663013491031287081289167626, + -0.94124485721060326391115015763113464139, + -0.94124485721060326391115015763113464139, + ]; + + let z = vec![ + 0.0, + 0.81261433409962649639237559737974432611, + 0.0, + -0.81261433409962649639237559737974432611, + 0.0, + 0.0, + -0.60167526419826270163441300578531749659, + 0.60167526419826270163441300578531749659, + -0.60167526419826270163441300578531749659, + -0.60167526419826270163441300578531749659, + 0.60167526419826270163441300578531749659, + -0.60167526419826270163441300578531749659, + 0.60167526419826270163441300578531749659, + 0.60167526419826270163441300578531749659, + -0.85545576101775998467509147069034657598, + 0.85545576101775998467509147069034657598, + -0.85545576101775998467509147069034657598, + -0.85545576101775998467509147069034657598, + 0.85545576101775998467509147069034657598, + -0.85545576101775998467509147069034657598, + 0.85545576101775998467509147069034657598, + 0.85545576101775998467509147069034657598, + -0.31339340451605472104577323055795129941, + 0.31339340451605472104577323055795129941, + -0.31339340451605472104577323055795129941, + -0.31339340451605472104577323055795129941, + 0.31339340451605472104577323055795129941, + -0.31339340451605472104577323055795129941, + 0.31339340451605472104577323055795129941, + 0.31339340451605472104577323055795129941, + 0.0, + -0.73466828699700801734638476986754918792, + -0.73466828699700801734638476986754918792, + 0.0, + 0.73466828699700801734638476986754918792, + 0.73466828699700801734638476986754918792, + -0.73466828699700801734638476986754918792, + 0.73466828699700801734638476986754918792, + 0.0, + 0.0, + 0.73466828699700801734638476986754918792, + -0.73466828699700801734638476986754918792, + -0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + -0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + 0.96509966551271026293028182312534456821, + -0.45079993511450943037788434573026952398, + -0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + -0.45079993511450943037788434573026952398, + -0.96509966551271026293028182312534456821, + -0.45079993511450943037788434573026952398, + -0.96509966551271026293028182312534456821, + 0.96509966551271026293028182312534456821, + -0.96509966551271026293028182312534456821, + 0.96509966551271026293028182312534456821, + -0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + 0.45079993511450943037788434573026952398, + 0.96509966551271026293028182312534456821, + 0.45079993511450943037788434573026952398, + -0.96509966551271026293028182312534456821, + -0.45079993511450943037788434573026952398, + -0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + -0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + 0.35390281459663013491031287081289167626, + -0.94124485721060326391115015763113464139, + -0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + -0.94124485721060326391115015763113464139, + -0.35390281459663013491031287081289167626, + -0.94124485721060326391115015763113464139, + -0.35390281459663013491031287081289167626, + 0.35390281459663013491031287081289167626, + -0.35390281459663013491031287081289167626, + 0.35390281459663013491031287081289167626, + -0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + 0.94124485721060326391115015763113464139, + 0.35390281459663013491031287081289167626, + 0.94124485721060326391115015763113464139, + -0.35390281459663013491031287081289167626, + -0.94124485721060326391115015763113464139, + ]; + + let p = x + .into_iter() + .zip(y.into_iter()) + .zip(z.into_iter()) + .map(|((x, y), z)| Vector3::new(x, y, z)) + .collect(); + + (w, p) +} diff --git a/fenris/src/reorder.rs b/fenris/src/reorder.rs new file mode 100644 index 0000000..33767d9 --- /dev/null +++ b/fenris/src/reorder.rs @@ -0,0 +1,243 @@ +use crate::assembly::CsrParAssembler; +use crate::connectivity::{Connectivity, ConnectivityMut}; +use crate::mesh::Mesh; +use crate::sparse::SparsityPattern; +use core::fmt; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, DimName, Scalar}; +use std::collections::VecDeque; +use std::error::Error; +use std::marker::PhantomData; + +#[derive(Debug, Clone)] +pub struct MeshPermutation { + vertex_perm: Permutation, + connectivity_perm: Permutation, +} + +impl MeshPermutation { + pub fn vertex_permutation(&self) -> &Permutation { + &self.vertex_perm + } + + pub fn connectivity_permutation(&self) -> &Permutation { + &self.connectivity_perm + } + + pub fn apply(&self, mesh: &Mesh) -> Mesh + where + T: Scalar, + D: DimName, + C: ConnectivityMut, + DefaultAllocator: Allocator, + { + let new_vertices = self.vertex_permutation().apply_to_slice(mesh.vertices()); + + // Connectivity is a little more involved: we need to update all "old" vertex indices + // referenced to new vertices + let inv_vertex_perm = self.vertex_permutation().inverse(); + let mut new_connectivity = self + .connectivity_permutation() + .apply_to_slice(mesh.connectivity()); + for conn in &mut new_connectivity { + for vertex_idx in conn.vertex_indices_mut() { + let new_vertex_index = inv_vertex_perm.source_index(*vertex_idx); + *vertex_idx = new_vertex_index; + } + } + Mesh::from_vertices_and_connectivity(new_vertices, new_connectivity) + } +} + +/// Creates a mesh permutation by computing a Reverse Cuthill-McKee permutation. +pub fn reorder_mesh_par(mesh: &Mesh) -> MeshPermutation +where + T: Scalar, + D: DimName, + C: Sync + Connectivity, + DefaultAllocator: Allocator, + Mesh: Sync, +{ + let assembler = CsrParAssembler::::default(); + + // Construct the CSR adjacency matrix for the graph represented by the mesh + let csr_graph = assembler.assemble_pattern(mesh); + let vertex_perm = reverse_cuthill_mckee(&csr_graph); + let inv_vertex_perm = vertex_perm.inverse(); + + // Reorder connectivity by sorting the connectivities by minimum (permuted) vertex index, + // which, after vertex reordering, typically has the effect of re-ordering elements + // so that elements with similar indices reference vertices with similar indices, + // thereby improving memory locality. + let mut connectivity_perm: Vec<_> = (0..mesh.connectivity().len()).collect(); + connectivity_perm.sort_by_key(|&connectivity_index| { + let vertices = mesh.connectivity()[connectivity_index].vertex_indices(); + vertices + .iter() + // Need to sort by the *new* (permuted) index of the vertex, not the old one + .map(|old_vertex_idx| inv_vertex_perm.source_index(*old_vertex_idx)) + .min() + }); + let connectivity_perm = Permutation::from_vec(connectivity_perm) + .expect("Internal error: Connectivity permutation must always be valid."); + + MeshPermutation { + vertex_perm, + connectivity_perm, + } +} + +/// A representation of an index permutation. +/// +/// More precisely, given `n` objects stored contiguously, the permutation internally +/// stores a permutation array `perm` such that for *target index* `i` in `0 .. n`, +/// the corresponding *source index* is given by +/// +/// ```ignore +/// target[i] = source[perm[i]] +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Permutation { + perm: Vec, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct InvalidPermutation { + marker: PhantomData<()>, +} + +impl fmt::Display for InvalidPermutation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Invalid permutation") + } +} + +impl Error for InvalidPermutation {} + +impl Permutation { + pub fn from_vec(perm: Vec) -> Result { + let mut visited = vec![false; perm.len()]; + for &index in &perm { + if visited[index] { + return Err(InvalidPermutation { marker: PhantomData }); + } else { + visited[index] = true; + } + } + Ok(Self { perm }) + } + + pub fn len(&self) -> usize { + self.perm.len() + } + + pub fn perm(&self) -> &[usize] { + &self.perm + } + + pub fn reverse(&mut self) { + self.perm.reverse() + } + + pub fn source_index(&self, target_index: usize) -> usize { + self.perm[target_index] + } + + pub fn inverse(&self) -> Permutation { + let mut inverse_perm = vec![std::usize::MAX; self.len()]; + for (target_idx, &source_idx) in self.perm().iter().enumerate() { + inverse_perm[source_idx] = target_idx; + } + + // TODO: Consider using an unsafe unchecked method + Self::from_vec(inverse_perm).unwrap() + } + + pub fn apply_to_slice(&self, slice: &[T]) -> Vec { + assert_eq!( + slice.len(), + self.len(), + "Slice and permutation must have the same size." + ); + self.perm() + .iter() + .map(|source_idx| slice[*source_idx].clone()) + .collect() + } +} + +/// Create a vertex permutation for a sparse symmetric matrix using the Cuthill-McKee algorithm. +pub fn cuthill_mckee(sparsity_pattern: &SparsityPattern) -> Permutation { + assert_eq!( + sparsity_pattern.major_dim(), + sparsity_pattern.minor_dim(), + "Matrix must be square." + ); + + let adjacent_vertices = |vertex_idx| { + sparsity_pattern + .lane(vertex_idx) + .expect("Vertex must be in bounds") + }; + let vertex_degree = |vertex_idx| adjacent_vertices(vertex_idx).len(); + + let mut queue = VecDeque::new(); + let mut permutation = Vec::with_capacity(sparsity_pattern.major_dim()); + let mut visited = vec![false; sparsity_pattern.major_dim()]; + + let mut adjacency_workspace = Vec::new(); + + // For matrices with zero rows or block diagonal patterns, the standard CutHill-McKee + // algorithm would not run to completion, because it assumes that all vertices are connected. + // To cope with this, we keep re-running the algorithm with a different starting vertex + // picked from the set of non-visited vertices + // Note: the current implementation is asymptotically very suboptimal if + // the sparsity pattern consists of a large number of disjoint components. + // TODO: Improve this? + while visited.iter().any(|entry| entry == &false) { + // Only look for minimum degree among vertices we have not yet visited + // TODO: Chosing the first minimum degree vertex is not necessarily the best choice. + // Investigate this! + let least_degree_vertex = (0..sparsity_pattern.major_dim()) + .filter(|vertex_idx| visited[*vertex_idx] == false) + .min_by_key(|vertex_idx| adjacent_vertices(*vertex_idx).len()); + + if let Some(start_vertex) = least_degree_vertex { + queue.push_back(start_vertex); + visited[start_vertex] = true; + + while let Some(vertex) = queue.pop_front() { + adjacency_workspace.clear(); + adjacency_workspace.extend(adjacent_vertices(vertex)); + adjacency_workspace.sort_unstable_by_key(|idx| vertex_degree(*idx)); + + permutation.push(vertex); + + // Cuthill-McKee is essentially just a breadth-first search in which + // the neighbors are visited in sorted order from lowest to highest + // vertex degree + for &adjacent_vertex in &adjacency_workspace { + if !visited[adjacent_vertex] { + visited[adjacent_vertex] = true; + queue.push_back(adjacent_vertex); + } + } + } + } + } + + assert_eq!( + permutation.len(), + sparsity_pattern.major_dim(), + "Internal error: Permutation has invalid length" + ); + Permutation::from_vec(permutation).expect("Internal error: Constructed permutation is invalid") +} + +/// Create a vertex permutation for a sparse symmetric matrix using the Reverse Cuthill-McKee (RCM) +/// algorithm. +pub fn reverse_cuthill_mckee(sparsity_pattern: &SparsityPattern) -> Permutation { + let mut perm = cuthill_mckee(sparsity_pattern); + perm.reverse(); + perm +} diff --git a/fenris/src/rtree.rs b/fenris/src/rtree.rs new file mode 100644 index 0000000..b461b2a --- /dev/null +++ b/fenris/src/rtree.rs @@ -0,0 +1,233 @@ +//! Helper functionality for working with an RTree for spatial acceleration. +use crate::allocators::ElementConnectivityAllocator; +use crate::element::ElementConnectivity; +use crate::geometry::{ + AxisAlignedBoundingBox, AxisAlignedBoundingBox2d, AxisAlignedBoundingBox3d, BoundedGeometry, Distance, + DistanceQuery, GeometryCollection, +}; +use crate::model::{FiniteElementInterpolator, MakeInterpolator}; +use crate::space::{FiniteElementSpace, GeometricFiniteElementSpace}; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, DimMin, DimName, Point, Point3, RealField, Scalar, U2, U3}; +use rstar::{PointDistance, RTree, RTreeObject, AABB}; +use serde::{Deserialize, Serialize}; +use std::error::Error; +use std::marker::PhantomData; + +pub type LabeledAABB3d = LabeledGeometry>; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct LabeledGeometry { + pub label: usize, + pub geometry: Geometry, + marker: PhantomData, +} + +impl LabeledGeometry { + pub fn new(label: usize, geometry: Geometry) -> Self { + Self { + label, + geometry, + marker: PhantomData, + } + } +} + +pub trait RTreeDim: DimName +where + DefaultAllocator: Allocator, +{ + type Envelope: rstar::Envelope; + + fn envelope_from_bounding_box(bounding_box: &AxisAlignedBoundingBox) -> Self::Envelope; + + fn translate_point(point: &Point) -> ::Point; +} + +impl RTreeDim for U2 { + type Envelope = AABB<[T; 2]>; + + fn envelope_from_bounding_box(bounding_box: &AxisAlignedBoundingBox) -> Self::Envelope { + rstar_aabb_from_bounding_box_2d(bounding_box) + } + + fn translate_point(point: &Point) -> [T; 2] { + point.coords.into() + } +} + +impl RTreeDim for U3 { + type Envelope = AABB<[T; 3]>; + + fn envelope_from_bounding_box(bounding_box: &AxisAlignedBoundingBox) -> Self::Envelope { + rstar_aabb_from_bounding_box_3d(bounding_box) + } + + fn translate_point(point: &Point) -> [T; 3] { + point.coords.into() + } +} + +impl RTreeObject for LabeledGeometry +where + T: RealField, + Geometry: BoundedGeometry, + Geometry::Dimension: RTreeDim, + DefaultAllocator: Allocator, +{ + type Envelope = >::Envelope; + + fn envelope(&self) -> Self::Envelope { + Geometry::Dimension::envelope_from_bounding_box(&self.geometry.bounding_box()) + } +} + +impl PointDistance for LabeledGeometry +where + T: RealField, + Geometry: BoundedGeometry + Distance>, +{ + // TODO: Consider implementing the other functions in this trait + fn distance_2(&self, point: &[T; 3]) -> T { + let [x, y, z] = *point; + let d = self.geometry.distance(&Point3::new(x, y, z)); + d * d + } +} + +pub struct GeometryCollectionAccelerator<'a, T, Collection> +where + T: RealField, + Collection: GeometryCollection<'a>, + Collection::Geometry: BoundedGeometry, + LabeledGeometry: RTreeObject, + DefaultAllocator: Allocator>::Dimension>, +{ + collection: &'a Collection, + r_tree: RTree>, +} + +impl<'a, T, Collection> GeometryCollectionAccelerator<'a, T, Collection> +where + T: RealField, + Collection: GeometryCollection<'a>, + Collection::Geometry: BoundedGeometry, + LabeledGeometry: RTreeObject, + DefaultAllocator: Allocator>::Dimension>, +{ + pub fn new(collection: &'a Collection) -> Self { + let geometries = (0..collection.num_geometries()) + .map(|i| LabeledGeometry::new(i, collection.get_geometry(i).unwrap())) + .collect(); + + Self { + collection, + r_tree: RTree::bulk_load(geometries), + } + } +} + +impl<'a, 'b, T, Collection> GeometryCollection<'b> for GeometryCollectionAccelerator<'a, T, Collection> +where + 'a: 'b, + T: RealField, + Collection: GeometryCollection<'a>, + Collection::Geometry: BoundedGeometry, + LabeledGeometry: RTreeObject, + DefaultAllocator: Allocator>::Dimension>, +{ + type Geometry = Collection::Geometry; + + fn num_geometries(&self) -> usize { + self.collection.num_geometries() + } + + fn get_geometry(&'b self, index: usize) -> Option { + self.collection.get_geometry(index) + } +} + +impl<'a, 'b, T, D, Collection> DistanceQuery<'b, Point> for GeometryCollectionAccelerator<'a, T, Collection> +where + 'a: 'b, + T: RealField, + D: RTreeDim, + Collection: GeometryCollection<'a>, + Collection::Geometry: BoundedGeometry, + LabeledGeometry: RTreeObject + PointDistance, + DefaultAllocator: Allocator, +{ + fn nearest(&'b self, query_geometry: &Point) -> Option { + self.r_tree + .nearest_neighbor(&D::translate_point(query_geometry)) + .map(|labeled_geometry| labeled_geometry.label) + } +} + +impl<'a, T, Collection> FiniteElementSpace for GeometryCollectionAccelerator<'a, T, Collection> +where + T: RealField, + Collection: FiniteElementSpace, + Collection: GeometryCollection<'a>, + Collection::Geometry: BoundedGeometry< + T, + Dimension = <>::Connectivity as ElementConnectivity>::GeometryDim, + >, + LabeledGeometry: RTreeObject, + DefaultAllocator: ElementConnectivityAllocator>::Connectivity>, +{ + type Connectivity = >::Connectivity; + + fn vertices(&self) -> &[Point>::Dimension>] { + self.collection.vertices() + } + + fn num_connectivities(&self) -> usize { + self.collection.num_connectivities() + } + + fn get_connectivity(&self, index: usize) -> Option<&Self::Connectivity> { + self.collection.get_connectivity(index) + } +} + +impl<'a, 'b, T, Collection> GeometricFiniteElementSpace<'b, T> for GeometryCollectionAccelerator<'a, T, Collection> +where + 'a: 'b, + T: RealField, + Collection: GeometricFiniteElementSpace<'a, T>, + Collection::Geometry: BoundedGeometry< + T, + Dimension = <>::Connectivity as ElementConnectivity>::GeometryDim, + >, + LabeledGeometry: RTreeObject, + DefaultAllocator: ElementConnectivityAllocator>::Connectivity>, +{ +} + +impl<'a, T, D, Collection> MakeInterpolator for GeometryCollectionAccelerator<'a, T, Collection> +where + T: RealField, + D: DimName + DimMin + RTreeDim, + Collection: GeometricFiniteElementSpace<'a, T> + DistanceQuery<'a, Point>, + Collection::Geometry: BoundedGeometry, + Collection::Connectivity: ElementConnectivity, + LabeledGeometry: RTreeObject + PointDistance, + DefaultAllocator: Allocator, + DefaultAllocator: ElementConnectivityAllocator, +{ + fn make_interpolator( + &self, + interpolation_points: &[Point], + ) -> Result, Box> { + FiniteElementInterpolator::interpolate_space(self, interpolation_points) + } +} + +pub fn rstar_aabb_from_bounding_box_3d(bounding_box: &AxisAlignedBoundingBox3d) -> AABB<[T; 3]> { + AABB::from_corners(bounding_box.min().clone().into(), bounding_box.max().clone().into()) +} + +pub fn rstar_aabb_from_bounding_box_2d(bounding_box: &AxisAlignedBoundingBox2d) -> AABB<[T; 2]> { + AABB::from_corners(bounding_box.min().clone().into(), bounding_box.max().clone().into()) +} diff --git a/fenris/src/solid/assembly.rs b/fenris/src/solid/assembly.rs new file mode 100644 index 0000000..ba51657 --- /dev/null +++ b/fenris/src/solid/assembly.rs @@ -0,0 +1,395 @@ +use crate::assembly::{ + assemble_generalized_elliptic_term_into, assemble_generalized_elliptic_term_into_par, + assemble_generalized_mass_into, assemble_generalized_stiffness_into, assemble_generalized_stiffness_into_csr, + assemble_generalized_stiffness_into_csr_par, assemble_transformed_generalized_stiffness_into_csr_par, + assemble_transformed_generalized_stiffness_par, compute_element_integral, ElementMatrixTransformation, + GeneralizedEllipticContraction, GeneralizedEllipticOperator, NoTransformation, QuadratureTable, +}; +use crate::element::{ElementConnectivity, VolumetricFiniteElement}; +use crate::solid::ElasticMaterialModel; +use nalgebra::allocator::Allocator; +use nalgebra::{ + DMatrixSliceMut, DVector, DVectorSlice, DVectorSliceMut, DefaultAllocator, DimMin, DimName, DimNameMul, Dynamic, + MatrixMN, MatrixN, MatrixSliceMN, Point, RealField, VectorN, U1, +}; +use rayon::prelude::*; + +use crate::allocators::{ElementConnectivityAllocator, FiniteElementMatrixAllocator, VolumeFiniteElementAllocator}; +use crate::{CooMatrix, CsrMatrix}; +use paradis::DisjointSubsets; + +/// A wrapper for a material model that allows it to be interpreted as a +/// generalized elliptic operator for use in assembly. +pub struct MaterialEllipticOperator<'a, Material: ?Sized>(pub &'a Material); + +impl<'a, T, D, M> GeneralizedEllipticOperator for MaterialEllipticOperator<'a, M> +where + T: RealField, + D: DimName, + M: ?Sized + ElasticMaterialModel, + DefaultAllocator: Allocator + Allocator, +{ + #[allow(non_snake_case)] + fn compute_elliptic_term(&self, gradient: &MatrixN) -> MatrixN { + // TODO: Avoid double transpose somehow? + let F = gradient.transpose() + MatrixN::<_, D>::identity(); + -self.0.compute_stress_tensor(&F).transpose() + } +} + +impl<'a, T, D, M> GeneralizedEllipticContraction for MaterialEllipticOperator<'a, M> +where + T: RealField, + D: DimName, + M: ?Sized + ElasticMaterialModel, + DefaultAllocator: Allocator + Allocator + Allocator, +{ + #[allow(non_snake_case)] + fn contract(&self, gradient: &MatrixN, a: &VectorN, b: &VectorN) -> MatrixN { + let F = gradient.transpose() + MatrixN::<_, D>::identity(); + self.0.contract_stress_tensor_with(&F, a, b) + } + + #[allow(non_snake_case)] + fn contract_multiple_into( + &self, + output: &mut DMatrixSliceMut, + gradient: &MatrixN, + a: &MatrixSliceMN, + ) { + let F = gradient.transpose() + MatrixN::<_, D>::identity(); + self.0.contract_multiple_stress_tensors_into(output, &F, a) + } +} + +pub fn assemble_stiffness_into( + coo: &mut CooMatrix, + vertices: &[Point], + connectivity: &[C], + material_model: &(impl ?Sized + ElasticMaterialModel), + u: &DVector, + quadrature_table: &impl QuadratureTable, +) where + T: RealField, + C: ElementConnectivity>::GeometryDim>, + C::Element: VolumetricFiniteElement, + C::GeometryDim: DimNameMul + DimMin, + C::NodalDim: DimNameMul, + DefaultAllocator: ElementConnectivityAllocator + + FiniteElementMatrixAllocator, +{ + let elliptic_operator = MaterialEllipticOperator(material_model); + assemble_generalized_stiffness_into(coo, vertices, connectivity, &elliptic_operator, u, quadrature_table) +} + +pub fn assemble_stiffness_into_csr( + csr: &mut CsrMatrix, + vertices: &[Point], + connectivity: &[C], + material_model: &(impl ?Sized + ElasticMaterialModel), + u: &DVector, + quadrature_table: &impl QuadratureTable, +) where + T: RealField, + C: ElementConnectivity>::GeometryDim>, + C::Element: VolumetricFiniteElement, + C::GeometryDim: DimNameMul + DimMin, + C::NodalDim: DimNameMul, + DefaultAllocator: ElementConnectivityAllocator + + FiniteElementMatrixAllocator, +{ + let elliptic_operator = MaterialEllipticOperator(material_model); + assemble_generalized_stiffness_into_csr(csr, vertices, connectivity, &elliptic_operator, u, quadrature_table) +} + +pub fn assemble_stiffness_into_csr_par( + csr: &mut CsrMatrix, + vertices: &[Point], + connectivity: &[C], + material_model: &(impl Sync + ?Sized + ElasticMaterialModel), + u: &DVector, + quadrature_table: &(impl Sync + QuadratureTable), + colors: &[DisjointSubsets], +) where + T: RealField, + C: Sync + ElementConnectivity>::GeometryDim>, + C::Element: VolumetricFiniteElement, + C::GeometryDim: DimNameMul + DimMin, + C::NodalDim: DimNameMul, + DefaultAllocator: ElementConnectivityAllocator + + FiniteElementMatrixAllocator, + >::Buffer: Sync, +{ + let elliptic_operator = MaterialEllipticOperator(material_model); + assemble_generalized_stiffness_into_csr_par( + csr, + vertices, + connectivity, + &elliptic_operator, + u, + quadrature_table, + colors, + ) +} + +pub fn assemble_transformed_stiffness_into_csr_par( + csr: &mut CsrMatrix, + vertices: &[Point], + connectivity: &[C], + material_model: &(impl Sync + ?Sized + ElasticMaterialModel), + u: &DVector, + quadrature_table: &(impl Sync + QuadratureTable), + transformation: &(dyn Sync + ElementMatrixTransformation), + colors: &[DisjointSubsets], +) where + T: RealField, + C: Sync + ElementConnectivity>::GeometryDim>, + C::Element: VolumetricFiniteElement, + C::GeometryDim: DimNameMul + DimMin, + C::NodalDim: DimNameMul, + DefaultAllocator: ElementConnectivityAllocator + + FiniteElementMatrixAllocator, + >::Buffer: Sync, +{ + let elliptic_operator = MaterialEllipticOperator(material_model); + assemble_transformed_generalized_stiffness_into_csr_par( + csr, + vertices, + connectivity, + &elliptic_operator, + u, + quadrature_table, + transformation, + colors, + ) +} + +pub fn assemble_transformed_stiffness_par( + vertices: &[Point], + connectivity: &[C], + material_model: &(impl Sync + ?Sized + ElasticMaterialModel), + u: &DVector, + quadrature_table: &(impl Sync + QuadratureTable), + transformation: &(dyn Sync + ElementMatrixTransformation), +) -> CooMatrix +where + T: RealField, + C: Sync + ElementConnectivity>::GeometryDim>, + C::Element: VolumetricFiniteElement, + C::GeometryDim: DimNameMul + DimMin, + C::NodalDim: DimNameMul, + DefaultAllocator: ElementConnectivityAllocator + + FiniteElementMatrixAllocator, + >::Buffer: Sync, +{ + let elliptic_operator = MaterialEllipticOperator(material_model); + assemble_transformed_generalized_stiffness_par( + vertices, + connectivity, + &elliptic_operator, + u, + quadrature_table, + transformation, + ) +} + +pub fn assemble_stiffness_par( + vertices: &[Point], + connectivity: &[C], + material_model: &(impl Sync + ?Sized + ElasticMaterialModel), + u: &DVector, + quadrature_table: &(impl Sync + QuadratureTable), +) -> CooMatrix +where + T: RealField, + C: Sync + ElementConnectivity>::GeometryDim>, + C::Element: VolumetricFiniteElement, + C::GeometryDim: DimNameMul + DimMin, + C::NodalDim: DimNameMul, + DefaultAllocator: ElementConnectivityAllocator + + FiniteElementMatrixAllocator, + >::Buffer: Sync, +{ + assemble_transformed_stiffness_par( + vertices, + connectivity, + material_model, + u, + quadrature_table, + &NoTransformation, + ) +} + +pub fn assemble_mass_into( + coo: &mut CooMatrix, + vertices: &[Point], + connectivity: &[C], + // TODO: Generalize density somehow? Attach properties to quadrature points? + density: T, + quadrature_table: &impl QuadratureTable, +) where + T: RealField, + C: ElementConnectivity>::GeometryDim>, + C::GeometryDim: DimNameMul + DimMin, + C::NodalDim: DimNameMul, + DefaultAllocator: FiniteElementMatrixAllocator, +{ + assemble_generalized_mass_into(coo, vertices, connectivity, density, quadrature_table) +} + +pub fn assemble_pseudo_forces_into<'a, T, C>( + f: DVectorSliceMut, + vertices: &[Point], + connectivity: &[C], + material_model: &(impl ?Sized + ElasticMaterialModel), + u: impl Into>, + quadrature_table: &impl QuadratureTable, +) where + T: RealField, + C: ElementConnectivity>::GeometryDim>, + C::GeometryDim: DimNameMul + DimMin, + DefaultAllocator: VolumeFiniteElementAllocator, +{ + let u = u.into(); + let elliptic_operator = MaterialEllipticOperator(material_model); + assemble_generalized_elliptic_term_into(f, vertices, connectivity, &elliptic_operator, &u, quadrature_table) +} + +pub fn assemble_pseudo_forces_into_par<'a, T, C>( + f: DVectorSliceMut, + vertices: &[Point], + connectivity: &[C], + material_model: &(impl ?Sized + Sync + ElasticMaterialModel), + u: impl Into>, + quadrature_table: &(impl Sync + QuadratureTable), + colors: &[DisjointSubsets], +) where + T: RealField, + C: Sync + ElementConnectivity>::GeometryDim>, + C::GeometryDim: DimNameMul + DimMin, + DefaultAllocator: VolumeFiniteElementAllocator, + >::Buffer: Sync, +{ + let u = u.into(); + let elliptic_operator = MaterialEllipticOperator(material_model); + assemble_generalized_elliptic_term_into_par( + f, + vertices, + connectivity, + &elliptic_operator, + &u, + quadrature_table, + colors, + ) +} + +/// A scalar function that is evaluated in material space +pub trait ScalarMaterialSpaceFunction +where + T: RealField, + GeometryDim: DimName, + SolutionDim: DimName, + DefaultAllocator: Allocator + Allocator + Allocator, +{ + fn evaluate( + &self, + material_coords: &VectorN, + u: &VectorN, + u_grad: &MatrixMN, + ) -> T; +} + +impl ScalarMaterialSpaceFunction for F +where + T: RealField, + GeometryDim: DimName, + SolutionDim: DimName, + DefaultAllocator: Allocator + Allocator + Allocator, + F: Fn(&VectorN, &VectorN, &MatrixMN) -> T, +{ + fn evaluate( + &self, + material_coords: &VectorN, + u: &VectorN, + u_grad: &MatrixMN, + ) -> T { + self(material_coords, u, u_grad) + } +} + +#[allow(non_snake_case)] +pub fn compute_scalar_element_integrals_into<'a, 'b, T, C>( + e: impl Into>, + vertices: &[Point], + connectivity: &[C], + integrand: &(impl ?Sized + ScalarMaterialSpaceFunction), + u: impl Into>, + quadrature_table: &impl QuadratureTable, +) where + T: RealField, + C: ElementConnectivity>::GeometryDim>, + C::GeometryDim: DimNameMul + DimMin, + DefaultAllocator: VolumeFiniteElementAllocator, +{ + let mut e = e.into(); + let u = u.into(); + + let fun = |material_coords: &VectorN, + u: &VectorN, + u_grad: &MatrixMN| + -> T { integrand.evaluate(material_coords, u, u_grad) }; + + for (i, connectivity) in connectivity.iter().enumerate() { + let element = connectivity.element(vertices).expect( + "All vertices of element are assumed to be in bounds.\ + TODO: Ensure this upon construction of basis?", + ); + let u_element = connectivity.element_variables(u); + + let element_energy = + compute_element_integral(&element, &u_element, &quadrature_table.quadrature_for_element(i), fun); + + e[i] = element_energy; + } +} + +#[allow(non_snake_case)] +pub fn compute_scalar_element_integrals_into_par<'a, 'b, T, C>( + e: impl Into>, + vertices: &[Point], + connectivity: &[C], + integrand: &(impl ?Sized + Sync + ScalarMaterialSpaceFunction), + u: impl Into>, + quadrature_table: &(impl Sync + QuadratureTable), +) where + T: RealField, + C: Sync + ElementConnectivity>::GeometryDim>, + C::GeometryDim: DimNameMul + DimMin, + DefaultAllocator: VolumeFiniteElementAllocator, + >::Buffer: Sync, +{ + let mut e = e.into(); + let u = u.into(); + + let fun = |material_coords: &VectorN, + u: &VectorN, + u_grad: &MatrixMN| + -> T { integrand.evaluate(material_coords, u, u_grad) }; + + connectivity + .par_iter() + .enumerate() + .zip(e.as_mut_slice().par_iter_mut()) + .for_each(|((i, connectivity), e)| { + let element = connectivity.element(vertices).expect( + "All vertices of element are assumed to be in bounds.\ + TODO: Ensure this upon construction of basis?", + ); + let u_element = connectivity.element_variables(u); + + let element_energy = + compute_element_integral(&element, &u_element, &quadrature_table.quadrature_for_element(i), fun); + + *e = element_energy; + }); +} diff --git a/fenris/src/solid/impl_model.rs b/fenris/src/solid/impl_model.rs new file mode 100644 index 0000000..878c095 --- /dev/null +++ b/fenris/src/solid/impl_model.rs @@ -0,0 +1,184 @@ +use crate::allocators::{ElementConnectivityAllocator, FiniteElementMatrixAllocator}; +use crate::assembly::ElementMatrixTransformation; +use crate::element::ElementConnectivity; +use crate::model::NodalModel; +use crate::solid::assembly::{ + assemble_mass_into, assemble_pseudo_forces_into, assemble_pseudo_forces_into_par, assemble_stiffness_into, + assemble_stiffness_into_csr, assemble_transformed_stiffness_into_csr_par, assemble_transformed_stiffness_par, + compute_scalar_element_integrals_into, compute_scalar_element_integrals_into_par, ScalarMaterialSpaceFunction, +}; +use crate::solid::{ElasticMaterialModel, ElasticityModel, ElasticityModelParallel}; +use crate::{CooMatrix, CsrMatrix}; +use nalgebra::allocator::Allocator; +use nalgebra::{DVector, DVectorSlice, DVectorSliceMut, DefaultAllocator, DimMin, DimNameMul, RealField}; + +impl ElasticityModel for NodalModel +where + T: RealField, + C: ElementConnectivity, + C::NodalDim: DimNameMul, + D: DimNameMul + DimMin, + DefaultAllocator: ElementConnectivityAllocator + FiniteElementMatrixAllocator, +{ + fn ndof(&self) -> usize { + D::dim() * self.vertices().len() + } + + fn assemble_stiffness_into( + &self, + csr: &mut CsrMatrix, + u: &DVector, + material_model: &dyn ElasticMaterialModel, + ) { + let error_msg = "Need stiffness quadrature to assemble stiffness matrix"; + assemble_stiffness_into_csr(csr, self.vertices(), self.connectivity(), material_model, u, &|_| { + self.stiffness_quadrature().expect(&error_msg) + }) + } + + fn assemble_stiffness(&self, u: &DVector, material_model: &dyn ElasticMaterialModel) -> CooMatrix { + let ndof = self.ndof(); + let mut coo = CooMatrix::new(ndof, ndof); + let error_msg = "Need stiffness quadrature to assemble stiffness matrix"; + assemble_stiffness_into( + &mut coo, + self.vertices(), + self.connectivity(), + material_model, + u, + &|_| self.stiffness_quadrature().expect(&error_msg), + ); + coo + } + + fn assemble_mass(&self, density: T) -> CooMatrix { + let ndof = self.ndof(); + let mut coo = CooMatrix::new(ndof, ndof); + let error_msg = "Need mass quadrature to assemble mass matrix"; + assemble_mass_into(&mut coo, self.vertices(), self.connectivity(), density, &|_| { + self.mass_quadrature().expect(&error_msg) + }); + coo + } + + fn assemble_elastic_pseudo_forces( + &self, + u: DVectorSlice, + material_model: &dyn ElasticMaterialModel, + ) -> DVector { + let mut f = DVector::zeros(u.len()); + let error_msg = "Need elliptic quadrature to assemble pseudo forces"; + assemble_pseudo_forces_into( + DVectorSliceMut::from(&mut f), + self.vertices(), + self.connectivity(), + material_model, + u, + &|_| self.elliptic_quadrature().expect(&error_msg), + ); + f + } + + fn compute_scalar_element_integrals( + &self, + u: DVectorSlice, + integrand: &dyn ScalarMaterialSpaceFunction, + ) -> DVector { + let mut e = DVector::zeros(self.connectivity().len()); + let error_msg = "Need elliptic quadrature to compute scalar integral"; + compute_scalar_element_integrals_into( + DVectorSliceMut::from(&mut e), + self.vertices(), + self.connectivity(), + integrand, + u, + // TODO: Is this a reasonable choice for computing the element integrals? + &|_| self.elliptic_quadrature().expect(&error_msg), + ); + e + } +} + +impl ElasticityModelParallel for NodalModel +where + T: RealField, + C: Sync + ElementConnectivity, + C::NodalDim: DimNameMul, + D: DimNameMul + DimMin, + DefaultAllocator: ElementConnectivityAllocator + FiniteElementMatrixAllocator, + >::Buffer: Sync, +{ + fn assemble_elastic_pseudo_forces_into_par( + &self, + f: DVectorSliceMut, + u: DVectorSlice, + material_model: &(dyn Sync + ElasticMaterialModel), + ) { + let error_msg = "Need elliptic quadrature to assemble pseudo forces"; + assemble_pseudo_forces_into_par( + f, + self.vertices(), + self.connectivity(), + material_model, + u, + &|_| self.elliptic_quadrature().expect(&error_msg), + self.colors(), + ); + } + + fn assemble_transformed_stiffness_par( + &self, + u: &DVector, + material_model: &(dyn Sync + ElasticMaterialModel), + transformation: &(dyn Sync + ElementMatrixTransformation), + ) -> CooMatrix { + let error_msg = "Need stiffness quadrature to assemble stiffness matrix"; + assemble_transformed_stiffness_par( + self.vertices(), + self.connectivity(), + material_model, + u, + &|_| self.stiffness_quadrature().expect(&error_msg), + transformation, + ) + } + + fn assemble_transformed_stiffness_into_par( + &self, + csr: &mut CsrMatrix, + u: &DVector, + material_model: &(dyn Sync + ElasticMaterialModel), + transformation: &(dyn Sync + ElementMatrixTransformation), + ) { + let error_msg = "Need stiffness quadrature to assemble stiffness matrix"; + assemble_transformed_stiffness_into_csr_par( + csr, + self.vertices(), + self.connectivity(), + material_model, + u, + &|_| self.stiffness_quadrature().expect(&error_msg), + transformation, + self.colors(), + ) + } + + fn compute_scalar_element_integrals_par( + &self, + u: DVectorSlice, + integrand: &(dyn Sync + ScalarMaterialSpaceFunction), + ) -> DVector { + let mut e = DVector::zeros(self.connectivity().len()); + let error_msg = "Need elliptic quadrature to compute scalar integral"; + compute_scalar_element_integrals_into_par( + DVectorSliceMut::from(&mut e), + self.vertices(), + self.connectivity(), + integrand, + u, + // TODO: Is this a reasonable choice for quadrature rule? + &|_| self.elliptic_quadrature().expect(&error_msg), + ); + e + } +} diff --git a/fenris/src/solid/materials.rs b/fenris/src/solid/materials.rs new file mode 100644 index 0000000..47560da --- /dev/null +++ b/fenris/src/solid/materials.rs @@ -0,0 +1,990 @@ +use crate::solid::ElasticMaterialModel; +use nalgebra::{ + DMatrixSliceMut, DefaultAllocator, DimMin, DimMul, DimName, DimProd, DimSub, Dynamic, Matrix2, Matrix3, Matrix4, + MatrixN, MatrixSliceMN, RealField, Scalar, SymmetricEigen, Vector2, Vector3, VectorN, U1, U2, U3, U4, U9, +}; + +use crate::util::{cross_product_matrix, diag_left_mul, rotation_svd, try_transmute_ref, try_transmute_ref_mut}; +use nalgebra::allocator::Allocator; +use numeric_literals::replace_float_literals; +use serde::{Deserialize, Serialize}; +use std::any::TypeId; +use std::ops::AddAssign; +use std::ops::SubAssign; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct LameParameters { + pub mu: T, + pub lambda: T, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct YoungPoisson { + pub young: T, + pub poisson: T, +} + +impl From> for LameParameters +where + T: RealField, +{ + #[replace_float_literals(T::from_f64(literal).expect("literal must fit in T"))] + fn from(params: YoungPoisson) -> Self { + let YoungPoisson { young, poisson } = params; + let mu = 0.5 * young / (1.0 + poisson); + let lambda = 0.5 * mu * poisson / (1.0 - 2.0 * poisson); + Self { mu, lambda } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct LinearElasticMaterial { + pub lame: LameParameters, +} + +impl From for LinearElasticMaterial +where + X: Into>, +{ + fn from(params: X) -> Self { + Self { lame: params.into() } + } +} + +#[allow(non_snake_case)] +#[replace_float_literals(T::from_f64(literal).expect("literal must fit in T"))] +impl ElasticMaterialModel for LinearElasticMaterial +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator + Allocator + Allocator, +{ + fn compute_strain_energy_density(&self, deformation_gradient: &MatrixN) -> T { + let F = deformation_gradient; + let eps = -MatrixN::::identity() + (F + F.transpose()) / 2.0; + + let eps_trace = eps.trace(); + let eps_frobenius_sq = eps.fold(0.0, |acc, x| acc + x * x); + + self.lame.mu * eps_frobenius_sq + 0.5 * self.lame.lambda * eps_trace * eps_trace + } + + fn compute_stress_tensor(&self, deformation_gradient: &MatrixN) -> MatrixN { + let F = deformation_gradient; + let eps = -MatrixN::::identity() + (F + F.transpose()) / 2.0; + &eps * 2.0 * self.lame.mu + MatrixN::::identity() * self.lame.lambda * eps.trace() + } + + fn contract_stress_tensor_with( + &self, + _deformation_gradient: &MatrixN, + a: &VectorN, + b: &VectorN, + ) -> MatrixN { + let B = a * b.transpose(); + let I = &MatrixN::::identity(); + let mu = self.lame.mu; + let lambda = self.lame.lambda; + (I * B.trace() + B.transpose()) * mu + B * lambda + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CorotatedLinearElasticMaterial { + pub lame: LameParameters, +} + +impl From for CorotatedLinearElasticMaterial +where + X: Into>, +{ + fn from(params: X) -> Self { + Self { lame: params.into() } + } +} + +#[allow(non_snake_case)] +#[replace_float_literals(T::from_f64(literal).expect("literal must fit in T"))] +impl ElasticMaterialModel for CorotatedLinearElasticMaterial +where + T: RealField, + D: DimName + DimMin + DimSub, + DefaultAllocator: Allocator + + Allocator + + Allocator + + Allocator + + Allocator>::Output> + + Allocator<(usize, usize), D>, +{ + fn compute_strain_energy_density(&self, deformation_gradient: &MatrixN) -> T { + let F = deformation_gradient; + let eps = -MatrixN::::identity() + (F + F.transpose()) / 2.0; + + let eps_trace = eps.trace(); + let eps_frobenius_sq = eps.fold(0.0, |acc, x| acc + x * x); + + self.lame.mu * eps_frobenius_sq + 0.5 * self.lame.lambda * eps_trace * eps_trace + } + + fn compute_stress_tensor(&self, deformation_gradient: &MatrixN) -> MatrixN { + let mu = self.lame.mu; + let lambda = self.lame.lambda; + + let F = deformation_gradient; + let I = &MatrixN::::identity(); + + let (U, _, V_T) = rotation_svd(F); + let R = &(U * V_T); + let eps = (R.transpose() * F) - MatrixN::::identity(); + + R * (&eps * 2.0 * mu + I * lambda * eps.trace()) + } + + fn contract_stress_tensor_with( + &self, + deformation_gradient: &MatrixN, + a: &VectorN, + b: &VectorN, + ) -> MatrixN { + let mu = self.lame.mu; + let lambda = self.lame.lambda; + + let B = a * b.transpose(); + let F = deformation_gradient; + let I = &MatrixN::::identity(); + + let (U, _, V_T) = rotation_svd(F); + let R = &(U * V_T); + + R * ((I * B.trace() + B.transpose()) * mu + B * lambda) * R.transpose() + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct StVKMaterial { + pub lame: LameParameters, +} + +impl From for StVKMaterial +where + X: Into>, +{ + fn from(params: X) -> Self { + Self { lame: params.into() } + } +} + +#[allow(non_snake_case)] +#[replace_float_literals(T::from_f64(literal).expect("literal must fit in T"))] +impl ElasticMaterialModel for StVKMaterial +where + T: RealField, + D: DimName + DimMin, + DefaultAllocator: Allocator + Allocator + Allocator<(usize, usize), D> + Allocator, +{ + fn compute_strain_energy_density(&self, deformation_gradient: &MatrixN) -> T { + let I = &MatrixN::::identity(); + let F = deformation_gradient; + let E = (F.transpose() * F - I) * 0.5; + + let E_trace = E.trace(); + let E_frobenius_sq = E.fold(0.0, |acc, x| acc + x * x); + + self.lame.mu * E_frobenius_sq + 0.5 * self.lame.lambda * E_trace * E_trace + } + + fn compute_stress_tensor(&self, deformation_gradient: &MatrixN) -> MatrixN { + let I = &MatrixN::::identity(); + let F = deformation_gradient; + let E = (F.transpose() * F - I) * 0.5; + F * (&E * 2.0 * self.lame.mu + I * self.lame.lambda * E.trace()) + } + + fn contract_stress_tensor_with( + &self, + deformation_gradient: &MatrixN, + a: &VectorN, + b: &VectorN, + ) -> MatrixN { + let B = a * b.transpose(); + let I = &MatrixN::::identity(); + let mu = self.lame.mu; + let lambda = self.lame.lambda; + let F = deformation_gradient; + + let E = (F.transpose() * F - I) * 0.5; + I * (E.dot(&B) * 2.0 * mu + lambda * E.trace() * B.trace()) + + F * (B.transpose() * mu + &B * lambda + I * mu * B.trace()) * F.transpose() + } +} + +/// A Neo-Hookean type material model that is stable and robust to inversions. +/// +/// Implements the material model proposed by Smith et al. [2018] in the paper +/// "Stable Neo-Hookean Flesh Simulation". +/// +/// This model does *not* include the projection onto semi-definiteness, +/// and as such will produce contractions which are indefinite. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct StableNeoHookeanMaterial { + pub lame: LameParameters, +} + +impl From for StableNeoHookeanMaterial +where + X: Into>, +{ + fn from(params: X) -> Self { + Self { lame: params.into() } + } +} + +#[replace_float_literals(T::from_f64(literal).expect("literal must fit in T"))] +fn reparametrize_lame_for_stable_neo_hookean(d: T, lame: &LameParameters) -> LameParameters +where + T: RealField, +{ + // Use the reparametrization described in section 3.4 of the paper, + // so that the material parameters are consistent with linear solid. + // Here we have generalized the results found in the paper to arbitrary dimension d. + let mu = (d + 1.0) / d * lame.mu; + let lambda = lame.lambda + (1.0 - 2.0 / (d * (d + 1.0))) * lame.mu; + LameParameters { mu, lambda } +} + +#[allow(non_snake_case)] +#[replace_float_literals(T::from_f64(literal).expect("literal must fit in T"))] +impl ElasticMaterialModel for StableNeoHookeanMaterial +where + T: RealField, + D: DimName + DimMin + DimMul, + DimProd: DimName, + DefaultAllocator: Allocator + + Allocator + + Allocator<(usize, usize), D> + + Allocator + + Allocator, DimProd> + + Allocator>, +{ + fn compute_strain_energy_density(&self, F: &MatrixN) -> T { + let d = T::from_usize(D::dim()).unwrap(); + let LameParameters { mu, lambda } = reparametrize_lame_for_stable_neo_hookean(d, &self.lame); + // Note: This expression is generalized from the paper, in which it was implicitly + // assumed that the geometrical dimension is 3. + let a = mu / lambda * (d / (d + 1.0)); + let alpha = 1.0 + a; + + let C = F.transpose() * F; + let I_C = C.trace(); + let J = F.determinant(); + + let J_minus_alpha = J - alpha; + + /* + let identity_strain_energy_density= + 0.5 * self.lame.lambda * a * a + - 0.5 * self.lame.mu * (d + 1.0).ln(); + + + 0.5 * self.lame.mu * (I_C - d) + + 0.5 * self.lame.lambda * (J_minus_alpha * J_minus_alpha) + - 0.5 * self.lame.mu * (I_C + 1.0).ln() + - identity_strain_energy_density + */ + + 0.5 * self.lame.mu * (I_C - d) + 0.5 * self.lame.lambda * ((J_minus_alpha * J_minus_alpha) - (a * a)) + - 0.5 * self.lame.mu * ((I_C + 1.0) / (d + 1.0)).ln() + } + + fn compute_stress_tensor(&self, F: &MatrixN) -> MatrixN { + let d = T::from_usize(D::dim()).unwrap(); + let LameParameters { mu, lambda } = reparametrize_lame_for_stable_neo_hookean(d, &self.lame); + // Note: This expression is generalized from the paper, in which it was implicitly + // assumed that the geometrical dimension is 3. + let alpha = 1.0 + mu / lambda * (d / (d + 1.0)); + + let C = F.transpose() * F; + let I_C = C.trace(); + let J = F.determinant(); + + // dJ_dF = J * F^{-T} + // Note: more specialized expressions can be derived for e.g. 2 and 3 dimensions, + // which would allow us to avoid computing the inverse of F + // Note: In general, dJ/dF = transpose(cofactor matrix of F) + let dJ_dF = F + .clone() + .try_inverse() + .expect("TODO: Handle singular F?") + .transpose() + * J; + + F * mu * (1.0 - 1.0 / (I_C + 1.0)) + dJ_dF * lambda * (J - alpha) + } + + fn contract_stress_tensor_with(&self, F: &MatrixN, a: &VectorN, b: &VectorN) -> MatrixN { + let B = a * b.transpose(); + let d = T::from_usize(F.nrows()).unwrap(); + let LameParameters { mu, lambda } = reparametrize_lame_for_stable_neo_hookean(d, &self.lame); + let alpha = 1.0 + mu / lambda * (d / (d + 1.0)); + + let I = &MatrixN::::identity(); + let C = F.transpose() * F; + let I_C = C.trace(); + let J = F.determinant(); + let beta = I_C + 1.0; + let beta2 = beta * beta; + let G = 1.0 - 1.0 / beta; + let H = (J - alpha) * J; + + let F_inv = F.clone().try_inverse().expect("TODO: Handle singular F?"); + let F_inv_t = F_inv.transpose(); + + let Q = F * &B * F.transpose() * 2.0 / beta2 + I * G * B.trace(); + let R = &F_inv_t * &B * &F_inv * (2.0 * J - alpha) * J - &F_inv_t * B.transpose() * &F_inv * H; + + Q * mu + R * lambda + } + + #[inline(never)] + fn contract_multiple_stress_tensors_into( + &self, + output: &mut DMatrixSliceMut, + F: &MatrixN, + a: &MatrixSliceMN, + ) { + let d = D::dim(); + let num_nodes = a.ncols(); + let output_dim = num_nodes * D::dim(); + assert_eq!(output_dim, output.nrows()); + assert_eq!(output_dim, output.ncols()); + + let lame_reparam = reparametrize_lame_for_stable_neo_hookean(T::from_usize(d).unwrap(), &self.lame); + let dp_df = build_stable_neohookean_dp_df(lame_reparam, F); + + // We have that F = Identity + sum_J U_J \otimes (grad phi_J), + // where \otimes denotes the outer product, U_J is the d-dimensional weight + // associated with node J, and grad phi_J is the gradient of basis function J. + // The result is that, with A given by + // A = [ grad phi_1 grad phi_2 ... grad phi_N ], + // the Jacobian matrix df/du is given by + // df/du = A \otimes I, + // where \otimes is the Kronecker product and I the d x d identity matrix. + // However, we don't actually compute it this way, because + // this does not take into account the inherent sparsity in the Kronecker expression, + // thus we instead use a custom implementation that works with the tensor indices directly. + contract_stiffness_tensor(output, &dp_df, &a); + } +} + +/// A semi-definite Neo-Hookean type material model that is stable and robust to inversions. +/// +/// Implements the material model proposed by Smith et al. [2018] in the paper +/// "Stable Neo-Hookean Flesh Simulation". +/// +/// This model includes the projection onto semi-definiteness, +/// and as such will produce contractions which are semi-definite. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProjectedStableNeoHookeanMaterial { + pub lame: LameParameters, +} + +impl From for ProjectedStableNeoHookeanMaterial +where + X: Into>, +{ + fn from(params: X) -> Self { + Self { lame: params.into() } + } +} + +#[replace_float_literals(T::from_f64(literal).expect("literal must fit in T"))] +impl ElasticMaterialModel for ProjectedStableNeoHookeanMaterial +where + T: RealField, +{ + fn is_positive_semi_definite(&self) -> bool { + true + } + + #[allow(non_snake_case)] + fn compute_strain_energy_density(&self, _F: &Matrix3) -> T { + todo!() + } + + #[allow(non_snake_case)] + fn compute_stress_tensor(&self, F: &Matrix3) -> Matrix3 { + StableNeoHookeanMaterial { + lame: self.lame.clone(), + } + .compute_stress_tensor(F) + } + + #[allow(non_snake_case)] + fn contract_stress_tensor_with(&self, _F: &Matrix3, _a: &Vector3, _b: &Vector3) -> Matrix3 { + todo!() + } + + #[allow(non_snake_case)] + #[inline(never)] + fn contract_multiple_stress_tensors_into( + &self, + output: &mut DMatrixSliceMut, + F: &Matrix3, + a: &MatrixSliceMN, + ) { + let d = 3; + let num_nodes = a.ncols(); + let output_dim = num_nodes * d; + assert_eq!(output_dim, output.nrows()); + assert_eq!(output_dim, output.ncols()); + + let lame_reparam = reparametrize_lame_for_stable_neo_hookean(T::from_usize(d).unwrap(), &self.lame); + let mut dp_df_eigendecomp = build_stable_neohookean_dp_df_eigen_3d(lame_reparam, F); + for eval in &mut dp_df_eigendecomp.eigenvalues { + *eval = T::max(T::zero(), *eval); + } + let dp_df = dp_df_eigendecomp.recompose(); + contract_stiffness_tensor(output, &dp_df, &a); + } +} + +#[replace_float_literals(T::from_f64(literal).expect("literal must fit in T"))] +impl ElasticMaterialModel for ProjectedStableNeoHookeanMaterial +where + T: RealField, +{ + fn is_positive_semi_definite(&self) -> bool { + true + } + + #[allow(non_snake_case)] + fn compute_strain_energy_density(&self, _F: &Matrix2) -> T { + todo!() + } + + #[allow(non_snake_case)] + fn compute_stress_tensor(&self, F: &Matrix2) -> Matrix2 { + StableNeoHookeanMaterial { + lame: self.lame.clone(), + } + .compute_stress_tensor(F) + } + + #[allow(non_snake_case)] + fn contract_stress_tensor_with(&self, _F: &Matrix2, _a: &Vector2, _b: &Vector2) -> Matrix2 { + todo!() + } + + #[allow(non_snake_case)] + #[inline(never)] + fn contract_multiple_stress_tensors_into( + &self, + output: &mut DMatrixSliceMut, + F: &Matrix2, + a: &MatrixSliceMN, + ) { + let d = 2; + let num_nodes = a.ncols(); + let output_dim = num_nodes * d; + assert_eq!(output_dim, output.nrows()); + assert_eq!(output_dim, output.ncols()); + + let lame_reparam = reparametrize_lame_for_stable_neo_hookean(T::from_usize(d).unwrap(), &self.lame); + let dp_df = build_stable_neohookean_dp_df(lame_reparam, F); + // TODO: We currently don't have analytic projection, so we resort to numerical + let mut dp_df_eigendecomp = dp_df.symmetric_eigen(); + for eval in &mut dp_df_eigendecomp.eigenvalues { + *eval = T::max(T::zero(), *eval); + } + let dp_df = dp_df_eigendecomp.recompose(); + contract_stiffness_tensor(output, &dp_df, &a); + } +} + +#[allow(non_snake_case)] +#[replace_float_literals(T::from_f64(literal).expect("literal must fit in T"))] +fn build_stable_neohookean_dp_df( + reparametrized_lame: LameParameters, + F: &MatrixN, +) -> MatrixN> +where + T: RealField, + D: DimName + DimMul + DimMin, + DimProd: DimName, + DefaultAllocator: Allocator + + Allocator + + Allocator<(usize, usize), D> + + Allocator + + Allocator, DimProd> + + Allocator>, +{ + let d = T::from_usize(D::dim()).unwrap(); + let LameParameters { mu, lambda } = reparametrized_lame; + let alpha = 1.0 + mu / lambda * (d / (d + 1.0)); + + let C = F.transpose() * F; + let I_C = C.trace(); + let J = F.determinant(); + let beta = I_C + 1.0; + let beta2 = beta * beta; + + // TODO: Use cofactor matrix instead of + // relying on inverse + let dJ_dF = F + .clone() + .try_inverse() + .expect("TODO: Handle singular F?") + .transpose() + * J; + + let f = vectorize(F); + let g = vectorize(&dJ_dF); + + // Build dP/dF in vec( ) form, i.e. dp/df, + // where p = vec(P) and f = vec(F) + let mut dp_df = MatrixN::<_, DimProd>::zeros(); + // Tikhonov term + dp_df.fill_diagonal(mu * (1.0 - 1.0 / beta)); + + // M-term, const * f f^T + dp_df.ger(mu * 2.0 / beta2, &f, &f, 1.0); + + // G-term, const * g g^T + dp_df.ger(lambda, &g, &g, 1.0); + + add_volume_hessian(&mut dp_df, lambda * (J - alpha), F); + dp_df +} + +#[allow(non_snake_case)] +#[replace_float_literals(T::from_f64(literal).expect("literal must fit in T"))] +fn build_stable_neohookean_dp_df_eigen_3d( + reparametrized_lame: LameParameters, + F: &Matrix3, +) -> SymmetricEigen +where + T: RealField, +{ + let (u, s, v_t) = rotation_svd(F); + + // J = det(F) = s_1 * s_2 * s_3 + // let J = s[0] * s[1] * s[2]; + // TODO: Use the above expression instead once we've confirmed that things work + let J = F.determinant(); + let C = F.transpose() * F; + let I_C = C.trace(); + + let d = 3.0; + let LameParameters { mu, lambda } = reparametrized_lame; + let alpha = 1.0 + mu / lambda * (d / (d + 1.0)); + + // Regularization term corresponding to the Identity (Tikhonov) regularization + let mu_t = mu * (1.0 - 1.0 / (I_C + 1.0)); + + let mut eigenvalues = VectorN::::zeros(); + let mut eigenvectors = MatrixN::::zeros(); + + // The first six eigenpairs are given by the so-called Twist & Flip matrices + // (see Smith et al., Analytic Eigensystems for Isotropic Distortion Energies) + // Note that we use the conventions from the original Stable Neo-Hookean paper, however + { + let sv_scale = lambda * (J - alpha); + let mu_t_vec = Vector3::repeat(mu_t.clone()); + eigenvalues + .fixed_slice_mut::(0, 0) + .copy_from(&(s * sv_scale + mu_t_vec)); + eigenvalues + .fixed_slice_mut::(3, 0) + .copy_from(&(-s * sv_scale + mu_t_vec)); + + // The eigenmatrices D_i are given by + // D_0 = (1/sqrt(2)) * [ u1 o v2 - u2 o v1 ] + // D_1 = (1/sqrt(2)) * [ u0 o v2 - u2 o v0 ] + // D_2 = (1/sqrt(2)) * [ u0 o v1 - u1 o v0 ] + // + // D_3 = (1/sqrt(2)) * [ u1 o v2 + u2 o v1 ] + // D_4 = (1/sqrt(2)) * [ u0 o v2 + u2 o v0 ] + // D_5 = (1/sqrt(2)) * [ u0 o v1 + u1 o v0 ] + // where u0 is the 0th col of U, v0 is the 0th col of V and so on. "o" here denotes + // the outer product of column vectors. + + // We bake the constant into V to avoid some unnecessary multiplications + // Moreover, since we only have the transpose of V, we must take *rows* of V rather + // than columns. + let v_t_scaled = v_t / T::sqrt(2.0); + let u0v1 = u.column(0) * v_t_scaled.row(1); + let u0v2 = u.column(0) * v_t_scaled.row(2); + let u1v0 = u.column(1) * v_t_scaled.row(0); + let u1v2 = u.column(1) * v_t_scaled.row(2); + let u2v0 = u.column(2) * v_t_scaled.row(0); + let u2v1 = u.column(2) * v_t_scaled.row(1); + + let mut set_eigenmatrix = |index, eigenmatrix| { + eigenvectors + .column_mut(index) + .copy_from(&vectorize(&eigenmatrix)); + }; + + set_eigenmatrix(0, u1v2 - u2v1); + set_eigenmatrix(1, u0v2 - u2v0); + set_eigenmatrix(2, u0v1 - u1v0); + + set_eigenmatrix(3, u1v2 + u2v1); + set_eigenmatrix(4, u0v2 + u2v0); + set_eigenmatrix(5, u0v1 + u1v0); + } + + // The final three eigenpairs are associated with the roots of a cubic equation, + // as detailed in the Stable Neo-Hookean paper. However, this is fairly complicated, + // so instead we take the much more practical route described in + // Smith et al., Analytic Eigensystems for Isotropic Distortion Energies + // of exploiting the fact that the remaining eigenpairs are related to the eigenpairs + // of a dxd matrix, which may be numerically computed. + { + let beta = I_C + 1.0; + let beta2 = beta * beta; + // Construct the 3x3 matrix A + let mut a = Matrix3::zeros(); + // Diagonal + a[(0, 0)] = mu_t + 2.0 * s[0] * s[0] * mu / beta2 + s[1] * s[1] * s[2] * s[2] * lambda; + a[(1, 1)] = mu_t + 2.0 * s[1] * s[1] * mu / beta2 + s[0] * s[0] * s[2] * s[2] * lambda; + a[(2, 2)] = mu_t + 2.0 * s[2] * s[2] * mu / beta2 + s[0] * s[0] * s[1] * s[1] * lambda; + + // Off-diagonal (A is symmetric) + let gamma = lambda * (2.0 * J - alpha); + a[(0, 1)] = s[2] * gamma + 2.0 * s[0] * s[1] * mu / beta2; + a[(0, 2)] = s[1] * gamma + 2.0 * s[0] * s[2] * mu / beta2; + a[(1, 2)] = s[0] * gamma + 2.0 * s[1] * s[2] * mu / beta2; + a[(1, 0)] = a[(0, 1)]; + a[(2, 0)] = a[(0, 2)]; + a[(2, 1)] = a[(1, 2)]; + + let a_eigen = a.symmetric_eigen(); + + // Each eigenvalue of a is directly an eigenvalue of dp/df + eigenvalues + .fixed_slice_mut::(6, 0) + .copy_from(&a_eigen.eigenvalues); + + // For each eigenvector of A, the components of the eigenvectors correspond to + // weights of the remaining "scaling directions", i.e. + // q_i = vec(Q_i), i = 1, 2, 3 + // with + // Q_i = U * (e_i o e_i) V^T + let Q_1 = &u * diag_left_mul(&a_eigen.eigenvectors.column(0), &v_t); + let Q_2 = &u * diag_left_mul(&a_eigen.eigenvectors.column(1), &v_t); + let Q_3 = &u * diag_left_mul(&a_eigen.eigenvectors.column(2), &v_t); + + eigenvectors.column_mut(6).copy_from(&vectorize(&Q_1)); + eigenvectors.column_mut(7).copy_from(&vectorize(&Q_2)); + eigenvectors.column_mut(8).copy_from(&vectorize(&Q_3)); + } + + SymmetricEigen { + eigenvectors, + eigenvalues, + } +} + +#[allow(non_snake_case)] +fn contract_stiffness_tensor( + output: &mut DMatrixSliceMut, + dp_df: &MatrixN>, + a: &MatrixSliceMN, +) where + T: RealField, + D: DimName + DimMul, + DimProd: DimName, + DefaultAllocator: Allocator + Allocator, DimProd> + Allocator>, +{ + let d = D::dim(); + assert_eq!(output.nrows(), output.ncols()); + assert_eq!(output.nrows() % d, 0); + let num_nodes = output.nrows() / d; + + // Capital I, J denote numberings, lower-case i, j denote dimension numbering + for J in 0..num_nodes { + for I in J..num_nodes { + let mut result_IJ = MatrixN::<_, D>::zeros(); + let a_I = a.fixed_slice::(0, I); + let a_J = a.fixed_slice::(0, J); + + for l in 0..d { + for k in 0..d { + for j in 0..d { + for i in 0..d { + // Convert tensor indices to linear row/col indices in dp/df + let linear_col = d * l + j; + let linear_row = d * k + i; + unsafe { + let dp_df_ikjn = dp_df + .get_unchecked((linear_row, linear_col)) + .inlined_clone(); + let a_Ik = a_I.get_unchecked(k).inlined_clone(); + let a_Jl = a_J.get_unchecked(l).inlined_clone(); + *result_IJ.get_unchecked_mut((i, j)) += dp_df_ikjn * a_Ik * a_Jl; + } + } + } + } + } + + output + .fixed_slice_mut::(I * d, J * d) + .add_assign(&result_IJ); + + if I != J { + output + .fixed_slice_mut::(J * d, I * d) + .add_assign(&result_IJ.transpose()); + } + } + } +} + +fn vectorize(matrix: &MatrixN) -> VectorN> +where + T: RealField, + D: DimName + DimMul, + DimProd: DimName, + DefaultAllocator: Allocator + Allocator>, +{ + let mut result = VectorN::zeros(); + let m = matrix.nrows(); + let n = matrix.ncols(); + for j in 0..n { + for i in 0..m { + result[n * j + i] = matrix[(i, j)]; + } + } + result +} + +fn add_volume_hessian_3d(matrix: &mut MatrixN, scale: T, deformation_gradient: &Matrix3) { + // Pre-multiply the scale into the hat matrices, so that it suffices + // to add them to the output matrix afterwards + let f0_hat = cross_product_matrix(&deformation_gradient.column(0).clone_owned()) * scale; + let f1_hat = cross_product_matrix(&deformation_gradient.column(1).clone_owned()) * scale; + let f2_hat = cross_product_matrix(&deformation_gradient.column(2).clone_owned()) * scale; + + matrix.fixed_slice_mut::(3, 0).add_assign(&f2_hat); + matrix.fixed_slice_mut::(6, 0).sub_assign(&f1_hat); + matrix.fixed_slice_mut::(0, 3).sub_assign(&f2_hat); + matrix.fixed_slice_mut::(6, 3).add_assign(&f0_hat); + matrix.fixed_slice_mut::(0, 6).add_assign(&f1_hat); + matrix.fixed_slice_mut::(3, 6).sub_assign(&f0_hat); +} + +#[replace_float_literals(T::from_f64(literal).expect("literal must fit in T"))] +fn add_volume_hessian_2d(matrix: &mut MatrixN, scale: T, _deformation_gradient: &Matrix2) { + // Pre-multiply the scale into the hat matrices, so that it suffices + // to add them to the output matrix afterwards + let s = scale; + matrix.add_assign(&Matrix4::new( + 0.0, 0.0, 0.0, s, 0.0, 0.0, -s, 0.0, 0.0, -s, 0.0, 0.0, s, 0.0, 0.0, 0.0, + )); +} + +fn add_volume_hessian( + matrix: &mut MatrixN>::Output>, + scale: T, + deformation_gradient: &MatrixN, +) where + T: RealField, + D: DimName + DimMul, + DefaultAllocator: + Allocator + Allocator + Allocator>::Output, >::Output>, +{ + if TypeId::of::() == TypeId::of::() { + assert_eq!(TypeId::of::>(), TypeId::of::()); + let matrix = try_transmute_ref_mut(matrix).unwrap(); + let deformation_gradient = try_transmute_ref(deformation_gradient).unwrap(); + add_volume_hessian_3d(matrix, scale, deformation_gradient); + } else if TypeId::of::() == TypeId::of::() { + let matrix = try_transmute_ref_mut(matrix).unwrap(); + let deformation_gradient = try_transmute_ref(deformation_gradient).unwrap(); + add_volume_hessian_2d(matrix, scale, deformation_gradient); + } else { + unimplemented!("Only 2D and 3D are supported"); + } +} + +/// Approximate the material model stiffness contraction +/// with B, given the deformation gradient F. +/// +/// Uses finite differences with parameter h. +#[allow(non_snake_case)] +#[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] +pub fn approximate_stiffness_contraction( + material: &impl ElasticMaterialModel, + F: &MatrixN, + a: &VectorN, + b: &VectorN, + h: T, +) -> MatrixN +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator + Allocator, +{ + let mut result: MatrixN = MatrixN::::zeros(); + + // Construct a matrix e_ij whose coefficients are all zero, except for ij, which satisfies + // (e_ij)_ij == 1 + let e = |i, j| { + let mut result = MatrixN::::zeros(); + result[(i, j)] = 1.0; + result + }; + + let P = |F| material.compute_stress_tensor(&F); + + // Use finite differences to compute a numerical approximation of the + // contraction between dP/dF and B + for i in 0..D::dim() { + for j in 0..D::dim() { + for l in 0..D::dim() { + for n in 0..D::dim() { + let dF_jn_plus = F + e(j, n) * h; + let dF_jn_minus = F - e(j, n) * h; + // Second order central difference + let D_iljn = (P(dF_jn_plus)[(i, l)] - P(dF_jn_minus)[(i, l)]) / (2.0 * h); + result[(i, j)] += D_iljn * a[l] * b[n]; + } + } + } + } + + result +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InvertibleMaterial { + material: M, + threshold: T, +} + +impl InvertibleMaterial { + pub fn new(material: M, threshold: T) -> Self { + Self { material, threshold } + } +} + +/// Projects the eigenvalues of the stretch tensor S onto an admissible set of eigenvalues. +#[allow(non_snake_case)] +#[replace_float_literals(T::from_f64(literal).expect("literal must fit in T"))] +fn project_deformation_gradient(F: &MatrixN, threshold: T) -> MatrixN +where + T: RealField, + D: DimName + DimMin + DimSub, + DefaultAllocator: + Allocator + Allocator + Allocator>::Output> + Allocator<(usize, usize), D>, +{ + let mut F_svd = F.clone().svd(true, true); + let u = F_svd.u.as_mut().unwrap(); + let v_t = F_svd.v_t.as_mut().unwrap(); + + let inversion = u.determinant().signum() != v_t.determinant().signum(); + + if inversion { + let smallest_sv_index = F_svd.singular_values.imin(); + let mut u_col = u.index_mut((.., smallest_sv_index)); + u_col *= -1.0; + F_svd.singular_values[smallest_sv_index] *= -1.0; + } + + let mut correction_necessary = false; + for sv in &mut F_svd.singular_values { + if *sv < threshold { + *sv = threshold; + correction_necessary = true; + } + } + + // Since the SVD may be inaccurate, make sure to only recompose + // when we actually changed something + let F = if correction_necessary { + F_svd + .recompose() + .expect("Can not fail, since we have computed u and v_t") + } else { + F.clone() + }; + // Sanity check + assert!(F.iter().all(|x_i| x_i.is_finite())); + F +} + +#[allow(non_snake_case)] +#[replace_float_literals(T::from_f64(literal).expect("literal must fit in T"))] +impl ElasticMaterialModel for InvertibleMaterial +where + T: RealField, + D: DimName + DimMin + DimSub, + M: ElasticMaterialModel, + DefaultAllocator: Allocator + + Allocator + + Allocator>::Output> + + Allocator<(usize, usize), D> + + Allocator, +{ + fn compute_strain_energy_density(&self, F: &MatrixN) -> T { + let F = project_deformation_gradient(F, self.threshold); + self.material.compute_strain_energy_density(&F) + } + + fn compute_stress_tensor(&self, F: &MatrixN) -> MatrixN { + let F = project_deformation_gradient(F, self.threshold); + self.material.compute_stress_tensor(&F) + } + + fn contract_stress_tensor_with(&self, F: &MatrixN, a: &VectorN, b: &VectorN) -> MatrixN { + let F = project_deformation_gradient(F, self.threshold); + self.material.contract_stress_tensor_with(&F, a, b) + } +} + +#[cfg(test)] +mod tests { + use crate::solid::materials::{ + build_stable_neohookean_dp_df, build_stable_neohookean_dp_df_eigen_3d, + reparametrize_lame_for_stable_neo_hookean, YoungPoisson, + }; + use nalgebra::Matrix3; + + #[allow(non_snake_case)] + #[test] + fn stable_neo_hookean_analytic_decomposition_matches_numerical_3d() { + let young_poisson = YoungPoisson { + young: 1e2f64, + poisson: 0.2f64, + }; + let F = Matrix3::new(0.5, 0.1, -0.2, 1.0, 1.5, 0.0, -0.1, -0.7, 0.9); + let lame = reparametrize_lame_for_stable_neo_hookean(3.0, &young_poisson.into()); + let mut df_dp_eigen_analytic = build_stable_neohookean_dp_df_eigen_3d(lame, &F); + let mut df_dp_eigen_numerical = build_stable_neohookean_dp_df(lame, &F).symmetric_eigen(); + + let df_dp_analytic = df_dp_eigen_analytic.recompose(); + let df_dp_numerical = df_dp_eigen_numerical.recompose(); + + // Sort eigenvalues before comparison + df_dp_eigen_analytic + .eigenvalues + .as_mut_slice() + .sort_by(|a, b| b.partial_cmp(&a).unwrap()); + df_dp_eigen_numerical + .eigenvalues + .as_mut_slice() + .sort_by(|a, b| b.partial_cmp(&a).unwrap()); + + let eigenvalue_diff = df_dp_eigen_analytic.eigenvalues - df_dp_eigen_numerical.eigenvalues; + let tol = 1e-12 * df_dp_eigen_numerical.eigenvalues.amax(); + assert!(eigenvalue_diff.amax() <= tol); + + // Note: We don't compare eigenvectors directly, because they may differ in sign. + // Instead, we reconstruct dp_df and compare. + let tol = 1e-12 * df_dp_numerical.amax(); + let df_dp_diff = df_dp_analytic - df_dp_numerical; + assert!(df_dp_diff.amax() <= tol); + } +} diff --git a/fenris/src/solid/mod.rs b/fenris/src/solid/mod.rs new file mode 100644 index 0000000..6f375e8 --- /dev/null +++ b/fenris/src/solid/mod.rs @@ -0,0 +1,213 @@ +//! Solid mechanics functionality. + +pub mod assembly; +mod impl_model; +pub mod materials; + +use nalgebra::{ + DMatrixSliceMut, DVector, DVectorSlice, DVectorSliceMut, DefaultAllocator, DimName, Dynamic, MatrixN, + MatrixSliceMN, RealField, Scalar, VectorN, U1, +}; + +use crate::{CooMatrix, CsrMatrix}; + +use crate::assembly::{ElementMatrixTransformation, NoTransformation}; +use assembly::ScalarMaterialSpaceFunction; +use delegate::delegate; +use nalgebra::allocator::Allocator; +use std::fmt::Debug; +use std::ops::{AddAssign, Deref}; + +pub trait ElasticMaterialModel: Debug +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator + Allocator, +{ + /// Indicates whether the energy Hessian is positive semi-definite. + /// + /// This is used by solvers to determine whether system matrices may become indefinite, + /// which may require special care. + fn is_positive_semi_definite(&self) -> bool { + false + } + + fn compute_strain_energy_density(&self, deformation_gradient: &MatrixN) -> T; + fn compute_stress_tensor(&self, deformation_gradient: &MatrixN) -> MatrixN; + fn contract_stress_tensor_with( + &self, + deformation_gradient: &MatrixN, + a: &VectorN, + b: &VectorN, + ) -> MatrixN; + + /// Compute stress tensor contractions for several vector pairs simultaneously. + /// + /// The matrix `a` is a `D x N` matrix, where each column is associated with a node. + /// Let c(F, a, b) denote a contraction of two vectors a and b, and let a_I denote + /// the Ith column in `a`. Let `output_IJ` denote the `D x D` block in position IJ + /// in `output`. At the end of this method call, the following property must hold: + /// output_IJ = output_IJ + c(F, a_I, a_J) + /// for all I, J = 1, ..., N. + /// + /// The default implementation will individually call the contraction for each + /// block `IJ`. By overriding the default implementation, it is possible to reuse + /// data across these computations and achieve higher performance. + fn contract_multiple_stress_tensors_into( + &self, + output: &mut DMatrixSliceMut, + deformation_gradient: &MatrixN, + a: &MatrixSliceMN, + ) { + let num_nodes = a.ncols(); + let output_dim = num_nodes * D::dim(); + assert_eq!(output_dim, output.nrows()); + assert_eq!(output_dim, output.ncols()); + + let d = D::dim(); + for i in 0..num_nodes { + for j in i..num_nodes { + let a_i = a.fixed_slice::(0, i).clone_owned(); + let a_j = a.fixed_slice::(0, j).clone_owned(); + let contraction = self.contract_stress_tensor_with(deformation_gradient, &a_i, &a_j); + output + .fixed_slice_mut::(i * d, j * d) + .add_assign(&contraction); + + // TODO: We currently assume symmetry. Should maybe have a method that + // says whether it is symmetric or not? + if i != j { + output + .fixed_slice_mut::(j * d, i * d) + .add_assign(&contraction.transpose()); + } + } + } + } +} + +pub trait ElasticityModel: Debug +where + DefaultAllocator: Allocator + Allocator, +{ + fn ndof(&self) -> usize; + + fn assemble_stiffness_into( + &self, + csr: &mut CsrMatrix, + u: &DVector, + material_model: &dyn ElasticMaterialModel, + ); + + fn assemble_stiffness(&self, u: &DVector, material_model: &dyn ElasticMaterialModel) -> CooMatrix; + + // TODO: Come up with a general abstraction for variable density (e.g. per-quadrature point) + fn assemble_mass(&self, density: T) -> CooMatrix; + + fn assemble_elastic_pseudo_forces( + &self, + u: DVectorSlice, + material_model: &dyn ElasticMaterialModel, + ) -> DVector; + + fn compute_scalar_element_integrals( + &self, + u: DVectorSlice, + integrand: &dyn ScalarMaterialSpaceFunction, + ) -> DVector; +} + +/// An extension of elasticity model that allows assembling in parallel. +/// +/// In general, the purpose of this trait is not to *guarantee* parallel execution, but to +/// allow parallel assembly given the right circumstances. +pub trait ElasticityModelParallel: ElasticityModel +where + DefaultAllocator: Allocator + Allocator, +{ + fn assemble_elastic_pseudo_forces_into_par( + &self, + f: DVectorSliceMut, + u: DVectorSlice, + material_model: &(dyn Sync + ElasticMaterialModel), + ); + + fn assemble_stiffness_par( + &self, + u: &DVector, + material_model: &(dyn Sync + ElasticMaterialModel), + ) -> CooMatrix { + self.assemble_transformed_stiffness_par(u, material_model, &NoTransformation) + } + + fn assemble_stiffness_into_par( + &self, + csr: &mut CsrMatrix, + u: &DVector, + material_model: &(dyn Sync + ElasticMaterialModel), + ) { + self.assemble_transformed_stiffness_into_par(csr, u, material_model, &NoTransformation) + } + + fn assemble_transformed_stiffness_par( + &self, + u: &DVector, + material_model: &(dyn Sync + ElasticMaterialModel), + transformation: &(dyn Sync + ElementMatrixTransformation), + ) -> CooMatrix; + + fn assemble_transformed_stiffness_into_par( + &self, + csr: &mut CsrMatrix, + u: &DVector, + material_model: &(dyn Sync + ElasticMaterialModel), + transformation: &(dyn Sync + ElementMatrixTransformation), + ); + + fn compute_scalar_element_integrals_par( + &self, + u: DVectorSlice, + integrand: &(dyn Sync + ScalarMaterialSpaceFunction), + ) -> DVector; +} + +impl ElasticMaterialModel for Box> +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator + Allocator, +{ + delegate! { + to self.deref() { + fn compute_strain_energy_density(&self, deformation_gradient: &MatrixN) -> T; + fn compute_stress_tensor(&self, deformation_gradient: &MatrixN) -> MatrixN; + fn contract_stress_tensor_with( + &self, + deformation_gradient: &MatrixN, + a: &VectorN, + b: &VectorN + ) -> MatrixN; + } + } +} + +impl ElasticMaterialModel for &X +where + T: RealField, + D: DimName, + X: ElasticMaterialModel, + DefaultAllocator: Allocator + Allocator, +{ + delegate! { + to self.deref() { + fn compute_strain_energy_density(&self, deformation_gradient: &MatrixN) -> T; + fn compute_stress_tensor(&self, deformation_gradient: &MatrixN) -> MatrixN; + fn contract_stress_tensor_with( + &self, + deformation_gradient: &MatrixN, + a: &VectorN, + b: &VectorN + ) -> MatrixN; + } + } +} diff --git a/fenris/src/space.rs b/fenris/src/space.rs new file mode 100644 index 0000000..471f65f --- /dev/null +++ b/fenris/src/space.rs @@ -0,0 +1,35 @@ +use crate::allocators::ElementConnectivityAllocator; +use crate::element::{ConnectivityGeometryDim, ElementConnectivity, ElementForSpace}; +use crate::geometry::GeometryCollection; +use nalgebra::{DefaultAllocator, Point, Scalar}; + +pub trait FiniteElementSpace +where + T: Scalar, + DefaultAllocator: ElementConnectivityAllocator, +{ + type Connectivity: ElementConnectivity; + + fn vertices(&self) -> &[Point>]; + + fn num_connectivities(&self) -> usize; + + fn get_connectivity(&self, index: usize) -> Option<&Self::Connectivity>; + + fn get_element(&self, index: usize) -> Option> { + self.get_connectivity(index) + .map(|conn| conn.element(self.vertices()).unwrap()) + } +} + +/// A finite element space whose elements can be seen as a collection of geometric entities. +/// +/// This trait essentially functions as a marker trait for finite element spaces which can +/// also be interpreted as a collection of geometry objects, with a 1:1 correspondence between +/// elements and geometries. +pub trait GeometricFiniteElementSpace<'a, T>: FiniteElementSpace + GeometryCollection<'a> +where + T: Scalar, + DefaultAllocator: ElementConnectivityAllocator, +{ +} diff --git a/fenris/src/space_impl.rs b/fenris/src/space_impl.rs new file mode 100644 index 0000000..19b5cb8 --- /dev/null +++ b/fenris/src/space_impl.rs @@ -0,0 +1,139 @@ +use crate::allocators::ElementConnectivityAllocator; +use crate::connectivity::CellConnectivity; +use crate::element::ElementConnectivity; +use crate::embedding::EmbeddedModel; +use crate::mesh::{ClosedSurfaceMesh2d, Mesh, Mesh2d}; +use crate::model::NodalModel; +use crate::space::{FiniteElementSpace, GeometricFiniteElementSpace}; +use nalgebra::{DefaultAllocator, DimName, Point, Point2, RealField, Scalar, U2}; + +impl FiniteElementSpace for Mesh +where + T: Scalar, + D: DimName, + C: ElementConnectivity, + DefaultAllocator: ElementConnectivityAllocator, +{ + type Connectivity = C; + + fn vertices(&self) -> &[Point] { + self.vertices() + } + + fn num_connectivities(&self) -> usize { + self.connectivity().len() + } + + fn get_connectivity(&self, index: usize) -> Option<&Self::Connectivity> { + self.connectivity().get(index) + } +} + +impl<'a, T, D, C> GeometricFiniteElementSpace<'a, T> for Mesh +where + T: Scalar, + D: DimName, + C: CellConnectivity + ElementConnectivity, + DefaultAllocator: ElementConnectivityAllocator, +{ +} + +impl FiniteElementSpace for ClosedSurfaceMesh2d +where + T: RealField, + Connectivity: ElementConnectivity, + DefaultAllocator: ElementConnectivityAllocator, +{ + type Connectivity = as FiniteElementSpace>::Connectivity; + + fn vertices(&self) -> &[Point2] { + self.mesh().vertices() + } + + fn num_connectivities(&self) -> usize { + self.mesh().connectivity().len() + } + + fn get_connectivity(&self, index: usize) -> Option<&Self::Connectivity> { + self.mesh().connectivity().get(index) + } +} + +impl<'a, T, C> GeometricFiniteElementSpace<'a, T> for ClosedSurfaceMesh2d +where + T: RealField, + C: CellConnectivity + ElementConnectivity, + DefaultAllocator: ElementConnectivityAllocator, +{ +} + +impl FiniteElementSpace for NodalModel +where + T: Scalar, + D: DimName, + C: ElementConnectivity, + DefaultAllocator: ElementConnectivityAllocator, +{ + type Connectivity = C; + + fn vertices(&self) -> &[Point] { + self.vertices() + } + + fn num_connectivities(&self) -> usize { + self.connectivity().len() + } + + fn get_connectivity(&self, index: usize) -> Option<&Self::Connectivity> { + self.connectivity().get(index) + } +} + +impl<'a, T, D, C> GeometricFiniteElementSpace<'a, T> for NodalModel +where + T: Scalar, + D: DimName, + C: CellConnectivity + ElementConnectivity, + DefaultAllocator: ElementConnectivityAllocator, +{ +} + +impl FiniteElementSpace for EmbeddedModel +where + T: Scalar, + D: DimName, + C: ElementConnectivity, + DefaultAllocator: ElementConnectivityAllocator, +{ + type Connectivity = C; + + fn vertices(&self) -> &[Point] { + self.vertices() + } + + fn num_connectivities(&self) -> usize { + self.interior_connectivity().len() + self.interface_connectivity().len() + } + + fn get_connectivity(&self, index: usize) -> Option<&Self::Connectivity> { + let num_interior_connectivity = self.interior_connectivity().len(); + + if index >= num_interior_connectivity { + // Interface connectivity + let interface_index = index - num_interior_connectivity; + self.interface_connectivity().get(interface_index) + } else { + let interior_index = index; + self.interior_connectivity().get(interior_index) + } + } +} + +impl<'a, T, D, C> GeometricFiniteElementSpace<'a, T> for EmbeddedModel +where + T: Scalar, + D: DimName, + C: CellConnectivity + ElementConnectivity, + DefaultAllocator: ElementConnectivityAllocator, +{ +} diff --git a/fenris/src/sparse.rs b/fenris/src/sparse.rs new file mode 100644 index 0000000..ee18988 --- /dev/null +++ b/fenris/src/sparse.rs @@ -0,0 +1,1510 @@ +//! Functionality for sparse linear algebra. +//! +//! Some of it is intended to be ported to `nalgebra` later. + +use alga::general::{ClosedAdd, ClosedMul, ClosedSub}; +use nalgebra::{DMatrix, DVector, DVectorSlice, DefaultAllocator, Dim, Dynamic, Matrix, RealField, Scalar, Vector}; +use num::{One, Zero}; + +use itertools::{izip, Itertools}; +use nalgebra::allocator::Allocator; +use nalgebra::base::storage::Storage; +use nalgebra::storage::StorageMut; +use paradis::{ParallelAccess, ParallelStorage}; +use rayon::prelude::*; +use std::cmp::max; +use std::mem::swap; +use std::ops::{Add, AddAssign, Mul, Sub}; +use std::slice; +use std::sync::Arc; + +/// A COO representation of a sparse matrix. +/// +/// Does not support arithmetic, only used for assembling CSC matrices. +#[derive(Debug, Clone)] +pub struct CooMatrix { + nrows: usize, + ncols: usize, + i: Vec, + j: Vec, + v: Vec, +} + +impl CooMatrix +where + T: Scalar, +{ + pub fn new(nrows: usize, ncols: usize) -> Self { + Self { + nrows, + ncols, + i: Vec::new(), + j: Vec::new(), + v: Vec::new(), + } + } + + pub fn from_triplets(nrows: usize, ncols: usize, rows: Vec, cols: Vec, values: Vec) -> Self { + if rows.iter().any(|i| *i >= nrows) { + panic!("Row indices contain index out of bounds."); + } + if cols.iter().any(|j| *j >= ncols) { + panic!("Col indices contain index out of bounds.") + } + + Self { + nrows, + ncols, + i: rows, + j: cols, + v: values, + } + } + + #[inline(always)] + pub fn push(&mut self, i: usize, j: usize, v: T) { + assert!(i < self.nrows); + assert!(j < self.ncols); + self.i.push(i); + self.j.push(j); + self.v.push(v); + } + + pub fn nrows(&self) -> usize { + self.nrows + } + + pub fn ncols(&self) -> usize { + self.ncols + } + + pub fn nnz(&self) -> usize { + self.v.len() + } + + /// TODO: Must take combiner to decide how to combine duplicate elements + pub fn build_dense(&self) -> DMatrix + where + T: ClosedAdd + Zero, + { + let mut result = DMatrix::zeros(self.nrows, self.ncols); + + for (i, j, v) in izip!(&self.i, &self.j, &self.v) { + result[(*i, *j)] += v.clone(); + } + + result + } + + /// Convert the COO matrix to a CSR matrix. + /// + /// The combiner must be associative. TODO: More docs + pub fn to_csr(&self, combiner: impl Fn(T, T) -> T) -> CsrMatrix + where + T: Zero, + { + let combiner = &combiner; + + let (unsorted_ia, unsorted_ja, unsorted_v) = { + let mut ia = vec![0usize; self.nrows() + 1]; + let mut ja = vec![0usize; self.nnz()]; + let mut v = vec![T::zero(); self.nnz()]; + coo_to_unsorted_csr(&mut ia, &mut ja, &mut v, self.nrows(), &self.i, &self.j, &self.v); + (ia, ja, v) + }; + + // At this point, CSR assembly is essentially complete. However, we must ensure + // that column indices are sorted and without duplicates. + let mut sorted_ia = Vec::new(); + let mut sorted_ja = Vec::new(); + let mut sorted_v = Vec::new(); + + sorted_ia.push(0); + + // We need some temporary storage when working with each row. Since rows often have a + // very small number of non-zero entries, we try to amortize allocations across + // rows by reusing workspace vectors + let mut idx_workspace = Vec::new(); + let mut perm_workspace = Vec::new(); + let mut values_workspace = Vec::new(); + + for row in 0..self.nrows() { + let begin = unsorted_ia[row]; + let end = unsorted_ia[row + 1]; + let count = end - begin; + let range = begin..end; + + // Ensure that workspaces can hold enough data + perm_workspace.resize(max(count, perm_workspace.len()), 0); + idx_workspace.resize(max(count, idx_workspace.len()), 0); + values_workspace.resize(max(count, values_workspace.len()), T::zero()); + sort_csr_row( + &mut idx_workspace[..count], + &mut values_workspace[..count], + &unsorted_ja[range.clone()], + &unsorted_v[range.clone()], + &mut perm_workspace[..count], + ); + + let sorted_ja_current_len = sorted_ja.len(); + + combine_duplicates( + |idx| sorted_ja.push(idx), + |val| sorted_v.push(val), + &idx_workspace[..count], + &values_workspace[..count], + combiner, + ); + + let new_col_count = sorted_ja.len() - sorted_ja_current_len; + sorted_ia.push(sorted_ia.last().unwrap() + new_col_count); + } + + CsrMatrix::from_csr_data(self.nrows(), self.ncols(), sorted_ia, sorted_ja, sorted_v) + } +} + +impl<'a, T: Clone> AddAssign<&'a CooMatrix> for CooMatrix { + fn add_assign(&mut self, rhs: &'a CooMatrix) { + assert_eq!( + self.nrows, rhs.nrows, + "Addition requires that matrices have the same number of rows." + ); + assert_eq!( + self.ncols, rhs.ncols, + "Addition rquires that matrices have the same number of columns." + ); + self.i.extend_from_slice(&rhs.i); + self.j.extend_from_slice(&rhs.j); + self.v.extend_from_slice(&rhs.v); + } +} + +/// Converts matrix data given in triplet format to unsorted CSR, retaining any duplicated +/// indices. +fn coo_to_unsorted_csr( + ia: &mut [usize], + ja: &mut [usize], + csr_values: &mut [T], + num_rows: usize, + rows: &[usize], + cols: &[usize], + coo_values: &[T], +) { + assert_eq!(ia.len(), num_rows + 1); + assert_eq!(ja.len(), csr_values.len()); + assert_eq!(csr_values.len(), rows.len()); + assert_eq!(rows.len(), cols.len()); + assert_eq!(cols.len(), coo_values.len()); + + // Count the number of occurrences of each row + for row_index in rows { + ia[*row_index] += 1; + } + + // Convert the counts to an offset + let mut offset = 0; + for i_offset in ia.iter_mut() { + let count = *i_offset; + *i_offset = offset; + offset += count; + } + + { + // TODO: Instead of allocating a whole new vector storing the current counts, + // I think it's possible to be a bit more clever by storing each count + // in the last of the column indices for each row + let mut current_counts = vec![0usize; num_rows + 1]; + for (i, j, value) in izip!(rows, cols, coo_values) { + let current_offset = ia[*i] + current_counts[*i]; + ja[current_offset] = *j; + csr_values[current_offset] = value.clone(); + current_counts[*i] += 1; + } + } +} + +/// Sort the indices of the given CSR row. +/// +/// The indices and values in `col_idx` and `col_values` are sorted according to the +/// column indices and stored in `col_idx_result` and `col_values` respectively. +/// +/// All input slices are expected to be of the same length. The contents of mutable slices +/// can be arbitrary, as they are anyway overwritten. +fn sort_csr_row( + col_idx_result: &mut [usize], + col_values_result: &mut [T], + col_idx: &[usize], + col_values: &[T], + workspace: &mut [usize], +) { + assert_eq!(col_idx_result.len(), col_values_result.len()); + assert_eq!(col_values_result.len(), col_idx.len()); + assert_eq!(col_idx.len(), col_values.len()); + assert_eq!(col_values.len(), workspace.len()); + + let permutation = workspace; + // Set permutation to identity + for (i, p) in permutation.iter_mut().enumerate() { + *p = i; + } + + // Compute permutation needed to bring column indices into sorted order + // Note: Using sort_unstable here avoids internal allocations, which is crucial since + // each column might have a small number of elements + permutation.sort_unstable_by_key(|idx| col_idx[*idx]); + + // TODO: Move this into `utils` or something? + fn apply_permutation(out_slice: &mut [T], in_slice: &[T], permutation: &[usize]) { + assert_eq!(out_slice.len(), in_slice.len()); + assert_eq!(out_slice.len(), permutation.len()); + for (out_element, old_pos) in izip!(out_slice, permutation) { + *out_element = in_slice[*old_pos].clone(); + } + } + + apply_permutation(col_idx_result, col_idx, permutation); + apply_permutation(col_values_result, col_values, permutation); +} + +/// Given *sorted* indices and corresponding scalar values, combines duplicates with the given +/// associative combiner and calls the provided produce methods with combined indices and values. +fn combine_duplicates( + mut produce_idx: impl FnMut(usize), + mut produce_value: impl FnMut(T), + idx_array: &[usize], + values: &[T], + combiner: impl Fn(T, T) -> T, +) { + assert_eq!(idx_array.len(), values.len()); + + let mut i = 0; + while i < idx_array.len() { + let idx = idx_array[i]; + let mut combined_value = values[i].clone(); + let mut j = i + 1; + while j < idx_array.len() && idx_array[j] == idx { + let j_val = values[j].clone(); + combined_value = combiner(combined_value, j_val); + j += 1; + } + produce_idx(idx); + produce_value(combined_value); + i = j; + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SparsityPattern { + major_offsets: Vec, + minor_indices: Vec, + minor_dim: usize, +} + +impl SparsityPattern { + pub fn new(major_dim: usize, minor_dim: usize) -> Self { + Self { + major_offsets: vec![0; major_dim + 1], + minor_indices: vec![], + minor_dim, + } + } + + pub fn from_offsets_and_indices( + major_dim: usize, + minor_dim: usize, + major_offsets: Vec, + minor_indices: Vec, + ) -> Self { + // TODO: Check validity of data + assert_eq!(major_offsets.len(), major_dim + 1); + assert_eq!(*major_offsets.last().unwrap(), minor_indices.len()); + Self { + major_offsets, + minor_indices, + minor_dim, + } + } + + // TODO: Do we want to try to remove duplicates? Probably not... + pub fn from_offsets_and_unsorted_indices( + major_dim: usize, + minor_dim: usize, + major_offsets: Vec, + mut minor_indices: Vec, + ) -> Self { + assert_eq!(major_offsets.len(), major_dim + 1); + assert_eq!(*major_offsets.last().unwrap(), minor_indices.len()); + if major_offsets + .iter() + .tuple_windows() + .any(|(prev, next)| prev > next) + { + panic!("Offsets must be non-decreasing."); + } + + for (major_begin, major_end) in major_offsets.iter().tuple_windows() { + let minor = &mut minor_indices[*major_begin..*major_end]; + minor.sort_unstable(); + if minor + .iter() + .tuple_windows() + .any(|(prev, next)| prev >= next) + { + panic!("Minor indices contain duplicates"); + } + } + + Self { + major_offsets, + minor_indices, + minor_dim, + } + } + + pub fn major_offsets(&self) -> &[usize] { + &self.major_offsets + } + + pub fn minor_indices(&self) -> &[usize] { + &self.minor_indices + } + + pub fn major_dim(&self) -> usize { + assert!(self.major_offsets.len() > 0); + self.major_offsets.len() - 1 + } + + pub fn minor_dim(&self) -> usize { + self.minor_dim + } + + pub fn nnz(&self) -> usize { + self.minor_indices.len() + } + + /// Get the lane at the given index. + /// + /// TODO: Document that lane is a generalization of row/col? + /// Is there better terminology? + pub fn lane(&self, major_index: usize) -> Option<&[usize]> { + let offset_begin = *self.major_offsets().get(major_index)?; + let offset_end = *self.major_offsets.get(major_index + 1)?; + Some(&self.minor_indices()[offset_begin..offset_end]) + } + + /// Appends another sparsity pattern to this one, in the sense that it is extended + /// along its major dimension. + /// + /// Panics if `self` and `other` have different minor dimensions. + pub fn append_pattern(&mut self, other: &SparsityPattern) { + assert_eq!(self.minor_dim(), other.minor_dim()); + + let offset_begin = *self.major_offsets.last().unwrap(); + let new_offsets_iter = other + .major_offsets() + .iter() + .map(|offset| offset + offset_begin); + + self.major_offsets.pop(); + self.major_offsets.extend(new_offsets_iter); + self.minor_indices.extend_from_slice(&other.minor_indices); + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CsrMatrix { + // Rows are major, cols are minor in the sparsity pattern + sparsity_pattern: Arc, + v: Vec, +} + +pub struct CsrRow<'a, T> { + ncols: usize, + column_indices: &'a [usize], + values: &'a [T], +} + +pub struct CsrRowMut<'a, T> { + column_indices: &'a [usize], + values: *mut T, +} + +// TODO: Use macros to avoid code duplication in impls? +impl<'a, T> CsrRow<'a, T> { + /// Number of non-zeros in this row. + pub fn nnz(&self) -> usize { + self.column_indices.len() + } + + pub fn values(&self) -> &[T] { + self.values + } + + pub fn column_indices(&self) -> &[usize] { + self.column_indices + } + + pub fn value_at_local_index(&self, local_index: usize) -> &T { + &self.values[local_index] + } + + pub fn col_at_local_index(&self, local_index: usize) -> usize { + self.column_indices[local_index] + } +} + +impl<'a, T: Clone + Zero> CsrRow<'a, T> { + pub fn get(&self, global_index: usize) -> Option { + let local_index = self.column_indices.binary_search(&global_index); + local_index + .ok() + .map(|local_index| self.values[local_index].clone()) + .or_else(|| { + if global_index < self.ncols { + Some(T::zero()) + } else { + None + } + }) + } +} + +impl<'a, T> CsrRowMut<'a, T> { + /// Number of non-zeros in this row. + pub fn nnz(&self) -> usize { + self.column_indices.len() + } + + pub fn values_mut(&mut self) -> &mut [T] { + unsafe { slice::from_raw_parts_mut(self.values, self.column_indices.len()) } + } + + // TODO: This API feels rather inelegant. Is there a better approach? + pub fn columns_and_values_mut(&mut self) -> (&[usize], &mut [T]) { + let values_mut = unsafe { slice::from_raw_parts_mut(self.values, self.column_indices.len()) }; + (&self.column_indices, values_mut) + } + + pub fn value_at_local_index(&self, local_index: usize) -> &T { + assert!(local_index < self.column_indices.len()); + unsafe { &*self.values.add(local_index) } + } + + pub fn col_at_local_index(&self, local_index: usize) -> usize { + self.column_indices[local_index] + } +} + +impl CsrMatrix { + pub fn new(nrows: usize, ncols: usize) -> Self { + Self { + sparsity_pattern: Arc::new(SparsityPattern::new(nrows, ncols)), + v: vec![], + } + } + + /// Computes a linear combination `A = sum(c_i * B_i)` of matrices over an iterator of items `(c_i, &B_i)`. + pub fn new_linear_combination<'a>(mut matrix_iter: impl Iterator) -> Option + where + T: Clone + ClosedAdd + ClosedMul + Scalar + Zero + One + 'a, + { + // If the iterator yields at least one coefficient/matrix pair... + matrix_iter.next().map(|(coeff, first_matrix)| { + // ...compute the linear combination by... + matrix_iter.fold( + // scaling the first matrix + first_matrix.clone() * coeff, + // and summing the scaled remaining matrices. + |mut matrix, (coeff, next_matrix)| { + matrix.add_assign_scaled(coeff, next_matrix); + matrix + }, + ) + }) + } + + pub fn row<'a>(&'a self, index: usize) -> CsrRow<'a, T> { + let row_begin = self.sparsity_pattern.major_offsets()[index]; + let row_end = self.sparsity_pattern.major_offsets()[index + 1]; + let column_indices = &self.sparsity_pattern.minor_indices()[row_begin..row_end]; + let values = &self.v[row_begin..row_end]; + + CsrRow { + ncols: self.ncols(), + column_indices, + values, + } + } + + pub fn row_mut<'a>(&'a mut self, index: usize) -> CsrRowMut<'a, T> { + assert!(index < self.nrows()); + + // Because of the lifetime of the borrow, we know that the `Arc` holding + // our sparsity pattern will outlive the returned reference. Thus, its data also cannot + // be mutated by other holders of the same `Arc`. Therefore we should be able to + // extend the lifetime of the borrow of the column indices to the given lifetime. + + let row_begin = self.sparsity_pattern.major_offsets()[index]; + let row_end = self.sparsity_pattern.major_offsets()[index + 1]; + let column_indices = &self.sparsity_pattern.minor_indices()[row_begin..row_end]; + + // Pointer to the first value + let values_ptr = unsafe { self.v.as_mut_ptr().add(row_begin) }; + + CsrRowMut { + column_indices, + values: values_ptr, + } + } + + pub fn nrows(&self) -> usize { + self.sparsity_pattern.major_dim() + } + + pub fn ncols(&self) -> usize { + self.sparsity_pattern.minor_dim() + } + + pub fn nnz(&self) -> usize { + self.sparsity_pattern.nnz() + } + + pub fn row_offsets(&self) -> &[usize] { + self.sparsity_pattern.major_offsets() + } + + pub fn column_indices(&self) -> &[usize] { + self.sparsity_pattern.minor_indices() + } + + pub fn values(&self) -> &[T] { + &self.v + } + + pub fn values_mut(&mut self) -> &mut [T] { + &mut self.v + } + + pub fn sparsity_pattern(&self) -> Arc { + Arc::clone(&self.sparsity_pattern) + } + + // TODO: Write tests + pub fn diag_iter<'a>(&'a self) -> impl 'a + Iterator + where + T: Zero + Clone, + { + let ia = self.row_offsets(); + let ja = self.column_indices(); + (0..self.nrows()).map(move |i| { + let row_begin = ia[i]; + let row_end = ia[i + 1]; + let columns_in_row = &ja[row_begin..row_end]; + if let Ok(idx) = columns_in_row.binary_search(&i) { + self.values()[row_begin + idx].clone() + } else { + T::zero() + } + }) + } + + pub fn from_csr_data(num_rows: usize, num_cols: usize, ia: Vec, ja: Vec, v: Vec) -> Self { + // TODO: Check validity of data + assert_eq!(num_rows + 1, ia.len(), "length of ia must be equal to num_rows + 1"); + assert_eq!(ja.len(), v.len()); + let pattern = SparsityPattern::from_offsets_and_indices(num_rows, num_cols, ia, ja); + Self { + sparsity_pattern: Arc::new(pattern), + v, + } + } + + pub fn from_diagonal<'a>(diagonal: impl Into>) -> Self + where + T: Scalar, + { + let diagonal = diagonal.into(); + let vals = diagonal.iter().cloned().collect(); + let num_rows = diagonal.len(); + let ia = (0..(num_rows + 1)).collect(); + let ja = (0..num_rows).collect(); + Self::from_csr_data(num_rows, num_rows, ia, ja, vals) + } + + pub fn from_pattern_and_values(pattern: Arc, values: Vec) -> Self { + assert_eq!(pattern.nnz(), values.len()); + Self { + sparsity_pattern: pattern, + v: values, + } + } + + /// TODO: Rename to to_dense? (Also for CooMatrix) + pub fn build_dense(&self) -> DMatrix + where + T: Scalar + Zero, + { + let mut result = DMatrix::zeros(self.nrows(), self.ncols()); + + for (i, j, v) in self.iter() { + result[(i, j)] = v.clone(); + } + + result + } + + /// Gives an iterator over non-zero elements in row-major order. + pub fn iter(&self) -> impl Iterator { + let ia = self.row_offsets(); + let ja = self.column_indices(); + (0..self.nrows()).flat_map(move |i| { + let row_begin = ia[i]; + let row_end = ia[i + 1]; + izip!(&ja[row_begin..row_end], &self.v[row_begin..row_end]).map(move |(j, v)| (i, *j, v)) + }) + } + + /// TODO: If we provide an `iter_mut`, then this would be largely redundant. However, + /// `iter_mut` likely requires a carefully implemented custom iterator to alleviate + /// lifetime concerns + pub fn transform_values(&mut self, f: impl Fn(usize, usize, &mut T)) { + let ia = self.sparsity_pattern.major_offsets(); + let ja = self.sparsity_pattern.minor_indices(); + for i in 0..self.nrows() { + let row_begin = ia[i]; + let row_end = ia[i + 1]; + for (j, v) in izip!(&ja[row_begin..row_end], &mut self.v[row_begin..row_end]) { + f(i, *j, v) + } + } + } + + /// Sets all values in the matrix to the specified value. + pub fn fill(&mut self, value: T) + where + T: Clone, + { + for v_i in self.values_mut().iter_mut() { + *v_i = value.clone(); + } + } + + /// Scales all values in the matrix by the given factor. + pub fn scale(&mut self, factor: T) + where + T: Clone + ClosedMul, + { + for v_i in self.values_mut().iter_mut() { + *v_i *= factor.clone(); + } + } + + /// Computes `self += a*x` where `x` is another matrix. Panics if the matrices are of different size. + pub fn add_assign_scaled(&mut self, a: T, x: &Self) + where + T: Clone + ClosedAdd + ClosedMul, + { + assert_eq!(self.values_mut().len(), x.values().len()); + for (v_i, x_i) in self.values_mut().iter_mut().zip(x.values().iter()) { + *v_i += a.clone() * x_i.clone(); + } + } + + /// Returns a new matrix containing only the elements indicated by the supplied predicate. + /// + /// Note that the number of rows and columns in the output is unchanged. + pub fn filter(&self, predicate: impl Fn(usize, usize, &T) -> bool) -> Self + where + T: Clone, + { + let ia = self.row_offsets(); + let ja = self.column_indices(); + + let mut new_ia = Vec::new(); + let mut new_ja = Vec::new(); + let mut new_v = Vec::new(); + + new_ia.push(0); + for i in 0..self.nrows() { + let row_begin = ia[i]; + let row_end = ia[i + 1]; + let current_ja_count = new_ja.len(); + for (j, v) in izip!(&ja[row_begin..row_end], &self.v[row_begin..row_end]) { + if predicate(i, *j, v) { + new_ja.push(*j); + new_v.push(v.clone()); + } + } + + let num_row_entries = new_ja.len() - current_ja_count; + let current_offset = new_ia[i]; + new_ia.push(current_offset + num_row_entries); + } + + assert_eq!(new_ia.len(), self.nrows() + 1); + assert_eq!(new_ja.len(), new_v.len()); + assert_eq!(new_ja.len(), *new_ia.last().unwrap()); + + // TODO: Circumvent data checks here by calling raw method instead, + // once checks are in place + Self::from_csr_data(self.nrows(), self.ncols(), new_ia, new_ja, new_v) + } + + pub fn upper_triangular_part(&self) -> Self + where + T: Clone, + { + self.filter(|i, j, _| i <= j) + } + + pub fn lower_triangular_part(&self) -> Self + where + T: Clone, + { + self.filter(|i, j, _| i >= j) + } + + pub fn append_csr_rows(&mut self, other: &CsrMatrix) + where + T: Clone, + { + Arc::make_mut(&mut self.sparsity_pattern).append_pattern(&other.sparsity_pattern()); + self.v.extend_from_slice(other.values()); + } + + pub fn to_csc(&self) -> CscMatrix + where + T: Clone, + { + // TODO: Generalize this so that we can reuse the implementation for CSC->CSR conversion + + let mut col_counts = vec![0; self.ncols() + 1]; + for (_, j, _) in self.iter() { + col_counts[j] += 1; + } + + let mut col_offsets = col_counts; + { + let mut current_offset = 0; + for offset in col_offsets.iter_mut() { + let count = *offset; + *offset = current_offset; + current_offset += count; + } + } + + // Clone values to prevent a T::zero() bound + let mut csc_values = self.v.clone(); + let mut row_indices = vec![0; self.nnz()]; + + // Use the column offsets to keep track of how many values we've stored in each + // column. This saves us from allocating one more vector + let mut col_current_offsets = col_offsets; + + let csr_values = self.values(); + + let column_indices = self.column_indices(); + for (r, (row_begin, row_end)) in self.row_offsets().iter().tuple_windows().enumerate() { + let cols = &column_indices[*row_begin..*row_end]; + for (jj, c) in cols.iter().enumerate() { + let csr_values_index = row_begin + jj; + let csc_values_index = col_current_offsets[*c]; + csc_values[csc_values_index] = csr_values[csr_values_index].clone(); + row_indices[csc_values_index] = r; + col_current_offsets[*c] += 1; + } + } + + // Restore the original column offsets array + let mut offset = 0; + for current_offset in col_current_offsets.iter_mut().take(self.ncols()) { + swap(&mut offset, current_offset); + } + let col_offsets = col_current_offsets; + + let csc_pattern = + SparsityPattern::from_offsets_and_indices(self.ncols(), self.nrows(), col_offsets, row_indices); + CscMatrix::from_pattern_and_values(Arc::new(csc_pattern), csc_values) + } + + pub fn concat_diagonally(matrices: &[CsrMatrix]) -> CsrMatrix + where + T: Clone, + { + let mut num_rows = 0; + let mut num_cols = 0; + let mut nnz = 0; + + // This first pass over the matrices is cheap, since we don't access any of the data. + // We use this to be able to pre-allocate enough capacity so that no further + // reallocation will be necessary. + for matrix in matrices { + num_rows += matrix.nrows(); + num_cols += matrix.ncols(); + nnz += matrix.nnz(); + } + + let mut values = Vec::with_capacity(nnz); + let mut column_indices = Vec::with_capacity(nnz); + let mut row_offsets = Vec::with_capacity(num_rows + 1); + + let mut col_offset = 0; + let mut nnz_offset = 0; + for matrix in matrices { + values.extend_from_slice(matrix.values()); + column_indices.extend(matrix.column_indices().iter().map(|i| *i + col_offset)); + row_offsets.extend( + matrix + .row_offsets() + .iter() + .take(matrix.nrows()) + .map(|offset| *offset + nnz_offset), + ); + + col_offset += matrix.ncols(); + nnz_offset += matrix.nnz(); + } + + row_offsets.push(nnz); + + Self { + // TODO: Avoid validation of pattern for performance + sparsity_pattern: Arc::new(SparsityPattern::from_offsets_and_indices( + num_rows, + num_cols, + row_offsets, + column_indices, + )), + v: values, + } + } +} + +impl CsrMatrix +where + T: Send + Sync, +{ + /// Computes a linear combination `A = sum(c_i * B_i)` of matrices over an iterator of items `(c_i, &B_i)`. Parallel version. + pub fn new_linear_combination_par<'a>(mut matrix_iter: impl Iterator) -> Option + where + T: Clone + ClosedAdd + ClosedMul + Scalar + Zero + One + 'a, + { + // If the iterator yields at least one coefficient/matrix pair... + matrix_iter.next().map(|(coeff, first_matrix)| { + // ...compute the linear combination by... + matrix_iter.fold( + // scaling the first matrix + first_matrix.clone() * coeff, + // and summing the scaled remaining matrices. + |mut matrix, (coeff, next_matrix)| { + matrix.add_assign_scaled_par(coeff, next_matrix); + matrix + }, + ) + }) + } + + /// Add assigns a linear combination `self += sum(c_i * A_i)` of matrices over an iterator of items `(c_i, &A_i)`. Parallel version. + pub fn add_assign_linear_combination_par<'a>(&mut self, matrix_iter: impl Iterator) + where + T: Clone + ClosedAdd + ClosedMul + Scalar + Zero + One + 'a, + { + for (coeff, matrix) in matrix_iter { + self.add_assign_scaled_par(coeff, matrix); + } + } + + /// Sets all values in the matrix to the specified value. Parallel version. + pub fn fill_par(&mut self, value: T) + where + T: Clone, + { + self.values_mut().par_iter_mut().for_each(|v_i| { + *v_i = value.clone(); + }); + } + + /// Scales all values in the matrix by the given factor. Parallel version. + pub fn scale_par(&mut self, factor: T) + where + T: Clone + ClosedMul, + { + self.values_mut().par_iter_mut().for_each(|v_i| { + *v_i *= factor.clone(); + }); + } + + /// Computes `self += a*x` where `x` is another matrix. Panics if the matrices are of different size. Parallel version. + pub fn add_assign_scaled_par(&mut self, a: T, x: &Self) + where + T: Clone + ClosedAdd + ClosedMul, + { + assert_eq!(self.values_mut().len(), x.values().len()); + + self.values_mut() + .par_iter_mut() + .zip(x.values().par_iter()) + .for_each(|(v_i, x_i)| { + *v_i += a.clone() * x_i.clone(); + }); + } +} + +#[derive(Copy)] +pub struct CsrParallelAccess<'a, T> { + sparsity_pattern: &'a SparsityPattern, + values_ptr: *mut T, +} + +impl<'a, T> Clone for CsrParallelAccess<'a, T> { + fn clone(&self) -> Self { + Self { + sparsity_pattern: self.sparsity_pattern, + values_ptr: self.values_ptr, + } + } +} + +unsafe impl<'a, T: 'a + Sync> Sync for CsrParallelAccess<'a, T> {} +unsafe impl<'a, T: 'a + Send> Send for CsrParallelAccess<'a, T> {} + +unsafe impl<'a, 'b, T: 'a + Sync + Send> ParallelAccess<'b> for CsrParallelAccess<'a, T> +where + 'a: 'b, +{ + type Record = CsrRow<'a, T>; + type RecordMut = CsrRowMut<'b, T>; + + unsafe fn get_unchecked(&'b self, global_index: usize) -> Self::Record { + let major_offsets = self.sparsity_pattern.major_offsets(); + let row_begin = *major_offsets.get_unchecked(global_index); + let row_end = *major_offsets.get_unchecked(global_index + 1); + let column_indices = &self.sparsity_pattern.minor_indices()[row_begin..row_end]; + let values_ptr = self.values_ptr.add(row_begin); + let values = slice::from_raw_parts(values_ptr, column_indices.len()); + CsrRow { + ncols: self.sparsity_pattern.minor_dim(), + column_indices, + values, + } + } + + unsafe fn get_unchecked_mut(&'b self, global_index: usize) -> Self::RecordMut { + let major_offsets = self.sparsity_pattern.major_offsets(); + let row_begin = *major_offsets.get_unchecked(global_index); + let row_end = *major_offsets.get_unchecked(global_index + 1); + let column_indices = &self.sparsity_pattern.minor_indices()[row_begin..row_end]; + let values_ptr = self.values_ptr.add(row_begin); + CsrRowMut { + column_indices, + values: values_ptr, + } + } +} + +unsafe impl<'a, T: 'a + Sync + Send> ParallelStorage<'a> for CsrMatrix { + type Access = CsrParallelAccess<'a, T>; + + fn create_access(&'a mut self) -> Self::Access { + let pattern = self.sparsity_pattern.as_ref(); + CsrParallelAccess { + sparsity_pattern: pattern, + values_ptr: self.v.as_mut_ptr(), + } + } + + fn len(&self) -> usize { + self.nrows() + } +} + +impl CsrMatrix +where + T: RealField, +{ + pub fn scale_rows<'a>(&mut self, diagonal_matrix: impl Into>) { + let diag = diagonal_matrix.into(); + assert_eq!(diag.len(), self.nrows()); + self.transform_values(|i, _, v| *v *= diag[i]); + } + + pub fn scale_cols<'a>(&mut self, diagonal_matrix: impl Into>) { + let diag = diagonal_matrix.into(); + assert_eq!(diag.len(), self.ncols()); + self.transform_values(|_, j, v| *v *= diag[j]); + } +} + +impl<'a, T, R, C, S> From<&'a Matrix> for CooMatrix +where + T: Scalar + Zero, + R: Dim, + C: Dim, + S: Storage, +{ + fn from(matrix: &'a Matrix) -> Self { + let mut coo = CooMatrix::new(matrix.nrows(), matrix.ncols()); + for i in 0..matrix.nrows() { + for j in 0..matrix.ncols() { + let val = matrix[(i, j)].clone(); + if val != T::zero() { + coo.push(i, j, matrix[(i, j)].clone()); + } + } + } + coo + } +} + +impl<'a, T, R, C, S> From<&'a Matrix> for CsrMatrix +where + T: Scalar + Zero, + R: Dim, + C: Dim, + S: Storage, +{ + fn from(matrix: &'a Matrix) -> Self { + // TODO: Construct directly as CSR matrix to avoid overhead of conversion from COO + CooMatrix::from(matrix).to_csr(|_, _| panic!("There cannot be duplicates when constructed from a dense matrix")) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CscMatrix { + // Cols are major, rows are minor in the sparsity pattern + sparsity_pattern: Arc, + v: Vec, +} + +impl CscMatrix { + pub fn nrows(&self) -> usize { + self.sparsity_pattern.minor_dim() + } + + pub fn ncols(&self) -> usize { + self.sparsity_pattern.major_dim() + } + + pub fn nnz(&self) -> usize { + self.sparsity_pattern.nnz() + } + + pub fn column_offsets(&self) -> &[usize] { + self.sparsity_pattern.major_offsets() + } + + pub fn row_indices(&self) -> &[usize] { + self.sparsity_pattern.minor_indices() + } + + pub fn values(&self) -> &[T] { + &self.v + } + + pub fn sparsity_pattern(&self) -> Arc { + Arc::clone(&self.sparsity_pattern) + } + + pub fn from_pattern_and_values(pattern: Arc, values: Vec) -> Self { + assert_eq!(pattern.nnz(), values.len()); + Self { + sparsity_pattern: pattern, + v: values, + } + } + + pub fn from_csc_data( + num_rows: usize, + num_cols: usize, + column_offsets: Vec, + row_indices: Vec, + values: Vec, + ) -> Self { + // TODO: Check validity of data + assert_eq!( + num_cols + 1, + column_offsets.len(), + "length of column_offsets must be equal to num_rows + 1" + ); + let pattern = SparsityPattern::from_offsets_and_indices(num_cols, num_rows, column_offsets, row_indices); + Self { + sparsity_pattern: Arc::new(pattern), + v: values, + } + } + + pub fn iter(&self) -> impl Iterator { + let indices = self.row_indices(); + self.column_offsets() + .iter() + .cloned() + .tuple_windows() + .enumerate() + .flat_map(move |(col, (col_begin, col_end))| { + izip!(&indices[col_begin..col_end], &self.v[col_begin..col_end]).map(move |(row, v)| (*row, col, v)) + }) + } + + pub fn to_dense(&self) -> DMatrix + where + T: Scalar + Zero, + DefaultAllocator: Allocator, + { + let mut result = DMatrix::zeros(self.nrows(), self.ncols()); + + for (i, j, v) in self.iter() { + result[(i, j)] = v.clone(); + } + + result + } +} + +/// Compute y <- beta * y + alpha * A * x, +/// where A is the CSR matrix +/// TODO: Generalize to any kind of vector, not just DVector +/// Matrix +pub fn spmv_csr( + beta: T, + y: &mut Vector, + alpha: T, + csr: &CsrMatrix, + x: &Vector, +) where + T: Scalar + Zero + ClosedMul + ClosedAdd, + SA: StorageMut, + SB: Storage, +{ + assert_eq!(y.len(), csr.nrows()); + assert_eq!(csr.ncols(), x.len()); + let y_data = y.data.as_mut_slice(); + + for (i, y_i) in y_data.iter_mut().enumerate() { + let row_begin = csr.row_offsets()[i]; + let row_end = csr.row_offsets()[i + 1]; + let col_indices = &csr.column_indices()[row_begin..row_end]; + let row_values = &csr.values()[row_begin..row_end]; + + *y_i = beta.clone() * y_i.clone(); + + // Compute axpy between row i in matrix and vector x + let mut a_i_dot_x = T::zero(); + for (a_ij, j) in izip!(row_values, col_indices) { + a_i_dot_x += a_ij.clone() * x[*j].clone(); + } + + *y_i += alpha.clone() * a_i_dot_x; + } +} + +/// Compute the CSR sparsity pattern for the product A B. +pub fn spmm_csr_pattern(pattern_a: &SparsityPattern, pattern_b: &SparsityPattern) -> SparsityPattern { + let mut major_offsets = Vec::with_capacity(pattern_a.major_dim()); + let mut minor_indices = Vec::new(); + + assert_eq!(pattern_a.minor_dim(), pattern_b.major_dim()); + + let mut current_offset = 0; + major_offsets.push(current_offset); + + // Build one lane at a time + for i in 0..pattern_a.major_dim() { + let a_lane = pattern_a.lane(i).unwrap(); + // Find non-zeros common to both a_i (lane i in A) and b_k (lane k in B) + for k in a_lane { + let b_lane = pattern_b.lane(*k).unwrap(); + + let mut local_offset = 0; + for b_kj in b_lane { + let c_i_col_indices = &minor_indices[(current_offset + local_offset)..]; + // TODO: An exponential search would probably be faster than the binary search here + let c_i_local_index = c_i_col_indices.binary_search(b_kj); + + // Upset local offset so that we binary search on a progressively smaller range + local_offset = match c_i_local_index { + // If the index was found, then we need not do anything + Ok(c_i_local_index) => local_offset + c_i_local_index, + // On the other hand, if it was not, insert into the appropriate position + Err(c_i_local_index) => { + let global_index = current_offset + local_offset + c_i_local_index; + // TODO: This might be expensive + minor_indices.insert(global_index, *b_kj); + local_offset + c_i_local_index + } + } + } + } + + let num_row_entries = minor_indices.len() - current_offset; + current_offset += num_row_entries; + major_offsets.push(current_offset); + } + + // TODO: Avoid potential consistency checks + SparsityPattern::from_offsets_and_indices( + pattern_a.major_dim(), + pattern_b.minor_dim(), + major_offsets, + minor_indices, + ) +} + +#[derive(Debug, Clone)] +pub struct IncompatibleSparsityPattern; + +/// Compute C <- beta * C + alpha * A * B. +/// +/// TODO: At the moment, this expects the output matrix c to have the same sparsity pattern as A * B. +/// It is not clear at present what kind of API is best suited to deal with the various situations +/// one might have +pub fn spmm_csr( + beta: T, + c: &mut CsrMatrix, + alpha: T, + a: &CsrMatrix, + b: &CsrMatrix, +) -> Result<(), IncompatibleSparsityPattern> +where + T: Scalar + ClosedAdd + ClosedMul, +{ + assert_eq!(c.nrows(), a.nrows()); + assert_eq!(c.ncols(), b.ncols()); + + for i in 0..c.nrows() { + let mut c_row = c.row_mut(i); + for c_ik in c_row.values_mut() { + *c_ik *= beta.inlined_clone(); + } + + let (c_columns, c_values) = c_row.columns_and_values_mut(); + + let a_row = a.row(i); + for (k, a_ik) in a_row.column_indices().iter().zip(a_row.values()) { + let b_row = b.row(*k); + + let mut local_offset = 0; + for (j, b_kj) in b_row.column_indices().iter().zip(b_row.values()) { + // Make sure to reduce the space we binary search in as + // we move through indices in b + let c_columns = &c_columns[local_offset..]; + // TODO: An exponential search would presumably be (much) faster + let index = c_columns + .binary_search(j) + .map_err(|_| IncompatibleSparsityPattern)?; + local_offset += index; + c_values[local_offset] += alpha.inlined_clone() * a_ik.inlined_clone() * b_kj.inlined_clone(); + } + } + } + + Ok(()) +} + +/// Compute C <- alpha * A * B +pub fn spmm_csr_owned(alpha: T, a: &CsrMatrix, b: &CsrMatrix) -> CsrMatrix +where + T: Scalar + ClosedAdd + ClosedMul + Zero, +{ + let pattern = spmm_csr_pattern(&a.sparsity_pattern(), &b.sparsity_pattern()); + let nnz = pattern.nnz(); + let mut result = CsrMatrix::from_pattern_and_values(Arc::new(pattern), vec![T::zero(); nnz]); + spmm_csr(T::zero(), &mut result, alpha, a, b).expect("Sparsity pattern is always compatible"); + result +} + +/// Compute C <- op(beta * C, alpha * A), where A and C are CSR matrices with *the same sparsity +/// pattern*. +pub fn csr_comp_bin_op_in_place_same_pattern T>( + op: Op, + beta: T, + c: &mut CsrMatrix, + alpha: T, + a: &CsrMatrix, +) where + T: Scalar + Zero + ClosedMul, +{ + assert_eq!(c.nrows(), a.nrows()); + assert_eq!(c.ncols(), a.ncols()); + assert_eq!(c.sparsity_pattern(), a.sparsity_pattern()); + assert_eq!(c.values().len(), a.values().len()); + + for (c_val, a_val) in c.values_mut().iter_mut().zip(a.values()) { + let new_value = op(beta.clone() * c_val.clone(), alpha.clone() * a_val.clone()); + *c_val = new_value; + } +} + +// TODO: Generalize to vectors of any dimension +impl<'a, T> Mul<&'a DVector> for &'a CsrMatrix +where + T: Scalar + ClosedAdd + ClosedMul + Zero + One, +{ + type Output = DVector; + + fn mul(self, rhs: &'a DVector) -> Self::Output { + let mut y = DVector::zeros(self.nrows()); + spmv_csr(T::zero(), &mut y, T::one(), self, rhs); + y + } +} + +impl<'a, T> Mul<&'a CsrMatrix> for &'a CsrMatrix +where + T: Scalar + ClosedAdd + ClosedMul + Zero + One, +{ + type Output = CsrMatrix; + + fn mul(self, rhs: &'a CsrMatrix) -> Self::Output { + spmm_csr_owned(T::one(), self, rhs) + } +} + +#[inline(always)] +fn csr_comp_bin_op_in_place T>(op: Op, beta: T, lhs: &mut CsrMatrix, alpha: T, rhs: &CsrMatrix) +where + T: Scalar + ClosedMul + Zero + One, +{ + if lhs.sparsity_pattern() == rhs.sparsity_pattern() { + csr_comp_bin_op_in_place_same_pattern(op, beta, lhs, alpha, rhs); + } else { + // TODO: Implement this! + unimplemented!() + } +} + +impl<'a, T> Add<&'a CsrMatrix> for CsrMatrix +where + T: Scalar + ClosedAdd + ClosedMul + Zero + One, +{ + type Output = CsrMatrix; + + fn add(mut self, rhs: &'a CsrMatrix) -> Self::Output { + csr_comp_bin_op_in_place(Add::add, T::one(), &mut self, T::one(), rhs); + self + } +} + +impl<'a, T> Add<&'a CsrMatrix> for &'a CsrMatrix +where + T: Scalar + ClosedAdd + ClosedMul + Zero + One, +{ + type Output = CsrMatrix; + + fn add(self, rhs: &'a CsrMatrix) -> Self::Output { + let mut result = self.clone(); + csr_comp_bin_op_in_place(Add::add, T::one(), &mut result, T::one(), rhs); + result + } +} + +impl Add> for CsrMatrix +where + T: Scalar + ClosedAdd + ClosedMul + Zero + One, +{ + type Output = CsrMatrix; + + fn add(mut self, rhs: CsrMatrix) -> Self::Output { + csr_comp_bin_op_in_place(Add::add, T::one(), &mut self, T::one(), &rhs); + self + } +} + +impl<'a, T> Sub<&'a CsrMatrix> for CsrMatrix +where + T: Scalar + ClosedSub + ClosedMul + Zero + One, +{ + type Output = CsrMatrix; + + fn sub(mut self, rhs: &'a CsrMatrix) -> Self::Output { + csr_comp_bin_op_in_place(Sub::sub, T::one(), &mut self, T::one(), rhs); + self + } +} + +impl<'a, T> Sub<&'a CsrMatrix> for &'a CsrMatrix +where + T: Scalar + ClosedSub + ClosedMul + Zero + One, +{ + type Output = CsrMatrix; + + fn sub(self, rhs: &'a CsrMatrix) -> Self::Output { + let mut result = self.clone(); + csr_comp_bin_op_in_place(Sub::sub, T::one(), &mut result, T::one(), rhs); + result + } +} + +impl Sub> for CsrMatrix +where + T: Scalar + ClosedSub + ClosedMul + Zero + One, +{ + type Output = CsrMatrix; + + fn sub(mut self, rhs: CsrMatrix) -> Self::Output { + csr_comp_bin_op_in_place(Sub::sub, T::one(), &mut self, T::one(), &rhs); + self + } +} + +impl Mul for CsrMatrix +where + T: Scalar + ClosedMul + Zero + One, +{ + type Output = Self; + + fn mul(mut self, scalar: T) -> Self::Output { + for val in self.values_mut() { + *val *= scalar.clone(); + } + self + } +} + +pub mod proptest_strategies { + use crate::CooMatrix; + use nalgebra::Scalar; + use num::Zero; + use proptest::collection::vec; + use proptest::prelude::*; + use std::cmp::{max, min}; + + /// Generates `coo` matrices, possibly with duplicate elements, with the given + /// maximum rows, columns and number of elements per row, with values being drawn + /// from the provided strategy. + pub fn coo( + max_rows: usize, + max_cols: usize, + max_elem_per_row: usize, + value_strategy: S, + ) -> impl Strategy> + where + T: Scalar + Zero, + S: Strategy + Clone, + { + let num_rows = 0..=max_rows; + let num_cols = 0..=max_cols; + + (num_rows, num_cols) + .prop_flat_map(move |(r, c)| (Just(r), Just(c), 0..=max(max_elem_per_row, c))) + .prop_flat_map(move |(r, c, num_elem_per_row)| { + // The minimum ensures that `nnz == 0` if `c == 0` or `r == 0` (i.e. empty matrix) + let nnz = num_elem_per_row * r * min(1, c); + // 0 .. 0 causes problems for `proptest`/`rand`, so + // we must give it a valid range in all circumstances, + // yet at the same time we must preserve the same type for things to compile. + // However, if `r == 0` or `c == 0`, then `nnz == 0`, so we will not actually + // generate any elements. + let r_range = if r > 0 { 0..r } else { 0..1 }; + let c_range = if c > 0 { 0..c } else { 0..1 }; + (Just(r), Just(c), vec((r_range, c_range, value_strategy.clone()), nnz)) + }) + .prop_map(|(num_rows, num_cols, triplets)| { + let mut coo = CooMatrix::new(num_rows, num_cols); + for (i, j, v) in triplets { + coo.push(i, j, v); + } + coo + }) + } +} diff --git a/fenris/src/util.rs b/fenris/src/util.rs new file mode 100644 index 0000000..a0ce399 --- /dev/null +++ b/fenris/src/util.rs @@ -0,0 +1,863 @@ +use crate::assembly::CsrParAssembler; +use crate::connectivity::Connectivity; +use crate::mesh::Mesh; +use crate::{CooMatrix, CsrMatrix}; +use itertools::Itertools; +use nalgebra::allocator::Allocator; +use nalgebra::constraint::{DimEq, SameNumberOfColumns, SameNumberOfRows, ShapeConstraint}; +use nalgebra::storage::{Storage, StorageMut}; +use nalgebra::{ + DMatrixSlice, DVector, DVectorSlice, DefaultAllocator, Dim, DimDiff, DimMin, DimName, DimSub, Dynamic, Matrix, + Matrix3, MatrixMN, MatrixN, MatrixSlice, MatrixSliceMut, Quaternion, RealField, Scalar, SliceStorage, + SliceStorageMut, SquareMatrix, UnitQuaternion, Vector, Vector3, VectorN, U1, +}; +use num::Zero; +use numeric_literals::replace_float_literals; +use std::error::Error; +use std::fmt::Display; +use std::fmt::LowerExp; +use std::fs::File; +use std::io::{BufWriter, Write}; +use std::ops::Add; +use std::path::Path; +use std::sync::Arc; + +/// Creates a column-major slice from the given matrix. +/// +/// Panics if the matrix does not have column-major storage. +pub fn coerce_col_major_slice( + matrix: &Matrix, + slice_rows: RSlice, + slice_cols: CSlice, +) -> MatrixSlice +where + T: Scalar, + R: Dim, + RSlice: Dim, + C: Dim, + CSlice: Dim, + S: Storage, + ShapeConstraint: DimEq + DimEq, +{ + assert_eq!(slice_rows.value(), matrix.nrows()); + assert_eq!(slice_cols.value(), matrix.ncols()); + let (rstride, cstride) = matrix.strides(); + assert!( + rstride == 1 && cstride == matrix.nrows(), + "Matrix must have column-major storage." + ); + + unsafe { + let data = + SliceStorage::new_with_strides_unchecked(&matrix.data, (0, 0), (slice_rows, slice_cols), (U1, slice_rows)); + Matrix::from_data_statically_unchecked(data) + } +} + +/// An SVD-like decomposition in which the orthogonal matrices `U` and `V` are rotation matrices. +/// +/// Given a matrix `A`, this method returns factors `U`, `S` and `V` such that +/// `A = U S V^T`, with `U, V` orthogonal and `det(U) = det(V) = 1` and `S` a diagonal matrix +/// whose entries are represented by a vector. +/// +/// Note that unlike the standard SVD, `S` may contain negative entries, and so they do not +/// generally coincide with singular values. However, it holds that `S(i)^2 == sigma_i^2`, where +/// `sigma_i` is the `i`th singular value of `A`. +/// +/// Returns a tuple `(U, S, V^T)`. +pub fn rotation_svd(matrix: &MatrixN) -> (MatrixN, VectorN, MatrixN) +where + T: RealField, + D: DimName + DimMin + DimSub, + DefaultAllocator: + Allocator + Allocator + Allocator>::Output> + Allocator<(usize, usize), D>, +{ + let minus_one = T::from_f64(-1.0).unwrap(); + let mut svd = matrix.clone().svd(true, true); + let min_val_idx = svd.singular_values.imin(); + + let mut u = svd.u.unwrap(); + if u.determinant() < T::zero() { + let mut u_col = u.column_mut(min_val_idx); + u_col *= minus_one; + svd.singular_values[min_val_idx] *= minus_one; + } + + let mut v_t = svd.v_t.unwrap(); + if v_t.determinant() < T::zero() { + let mut v_t_row = v_t.row_mut(min_val_idx); + v_t_row *= minus_one; + svd.singular_values[min_val_idx] *= minus_one; + } + + (u, svd.singular_values, v_t) +} + +/// "Analytic polar decomposition" +/// +/// Translated to Rust from https://github.com/InteractiveComputerGraphics/FastCorotatedFEM/blob/351b007b6bb6e8d97f457766e9ecf9b2bced7079/FastCorotFEM.cpp#L413 +/// +/// ``` +/// use fenris::util::apd; +/// use nalgebra::{Matrix3, UnitQuaternion, Quaternion, Vector3}; +/// +/// let eps: f64 = 1e-12; +/// let guess = UnitQuaternion::from_axis_angle(&Vector3::x_axis(), 0.2); +/// assert!((apd::(&Matrix3::identity(), &guess, 100, eps).as_ref() - &Quaternion::identity()).norm() < 1.0e1 * eps); +/// assert!((apd::(&Matrix3::identity(), &guess, 100, eps).as_ref() - guess.as_ref()).norm() > 1.0e2 * eps); +/// ``` +/// +#[allow(non_snake_case)] +#[replace_float_literals(T::from_f64(literal).unwrap())] +pub fn apd( + deformation_grad: &Matrix3, + initial_guess: &UnitQuaternion, + max_iter: usize, + tol: T, +) -> UnitQuaternion { + let F = deformation_grad; + let mut q: UnitQuaternion = initial_guess.clone(); + + let tol_squared = tol * tol; + let mut res = T::max_value(); + let mut iter = 0; + while res > tol_squared && iter < max_iter { + let R = q.to_rotation_matrix(); + let B = R.transpose() * F; + + let B0 = B.column(0); + let B1 = B.column(1); + let B2 = B.column(2); + + let gradient = Vector3::new(B2[1] - B1[2], B0[2] - B2[0], B1[0] - B0[1]); + + // compute Hessian, use the fact that it is symmetric + let h00 = B1[1] + B2[2]; + let h11 = B0[0] + B2[2]; + let h22 = B0[0] + B1[1]; + let h01 = (B1[0] + B0[1]) * 0.5; + let h02 = (B2[0] + B0[2]) * 0.5; + let h12 = (B2[1] + B1[2]) * 0.5; + + let detH = + -(h02 * h02 * h11) + (h01 * h02 * h12) * 2.0 - (h00 * h12 * h12) - (h01 * h01 * h22) + (h00 * h11 * h22); + let factor = detH.recip() * (-0.25); + + let mut omega = Vector3::zeros(); + + // compute symmetric inverse + omega[0] = (h11 * h22 - h12 * h12) * gradient[0] + + (h02 * h12 - h01 * h22) * gradient[1] + + (h01 * h12 - h02 * h11) * gradient[2]; + omega[1] = (h02 * h12 - h01 * h22) * gradient[0] + + (h00 * h22 - h02 * h02) * gradient[1] + + (h01 * h02 - h00 * h12) * gradient[2]; + omega[2] = (h01 * h12 - h02 * h11) * gradient[0] + + (h01 * h02 - h00 * h12) * gradient[1] + + (h00 * h11 - h01 * h01) * gradient[2]; + omega *= factor; + + // if det(H) = 0 use gradient descent, never happened in our tests, could also be removed + if detH.abs() < 1.0e-9 { + omega = -gradient; + } + + // instead of clamping just use gradient descent. also works fine and does not require the norm + let useGD = omega.dot(&gradient) > T::zero(); + if useGD { + omega = &gradient * (-0.125); + } + + let l_omega2 = omega.norm_squared(); + + let w = (1.0 - l_omega2) / (1.0 + l_omega2); + let vec = omega * (2.0 / (1.0 + l_omega2)); + + // no normalization needed because the Cayley map returs a unit quaternion + q = q * UnitQuaternion::new_unchecked(Quaternion::from_parts(w, vec)); + + iter += 1; + res = l_omega2; + } + + q +} + +pub fn diag_left_mul(diag: &Vector, matrix: &MatrixMN) -> MatrixMN +where + T: RealField, + D1: DimName, + D2: DimName, + S: Storage, + DefaultAllocator: Allocator, +{ + // TODO: This is inefficient + let mut result = matrix.clone(); + for (i, mut row) in result.row_iter_mut().enumerate() { + row *= diag[i]; + } + result +} + +/// Creates a mutable column-major slice from the given matrix. +/// +/// Panics if the matrix does not have column-major storage. +pub fn coerce_col_major_slice_mut( + matrix: &mut Matrix, + slice_rows: RSlice, + slice_cols: CSlice, +) -> MatrixSliceMut +where + T: Scalar, + R: Dim, + RSlice: Dim, + C: Dim, + CSlice: Dim, + S: StorageMut, + ShapeConstraint: DimEq + DimEq, +{ + assert_eq!(slice_rows.value(), matrix.nrows()); + assert_eq!(slice_cols.value(), matrix.ncols()); + let (rstride, cstride) = matrix.strides(); + assert!( + rstride == 1 && cstride == matrix.nrows(), + "Matrix must have column-major storage." + ); + + unsafe { + let data = SliceStorageMut::new_with_strides_unchecked( + &mut matrix.data, + (0, 0), + (slice_rows, slice_cols), + (U1, slice_rows), + ); + Matrix::from_data_statically_unchecked(data) + } +} + +pub fn try_transmute_ref(e: &T) -> Option<&U> { + use std::any::TypeId; + use std::mem::transmute; + if TypeId::of::() == TypeId::of::() { + Some(unsafe { transmute(e) }) + } else { + None + } +} + +pub fn try_transmute_ref_mut(e: &mut T) -> Option<&mut U> { + use std::any::TypeId; + use std::mem::transmute; + if TypeId::of::() == TypeId::of::() { + Some(unsafe { transmute(e) }) + } else { + None + } +} + +pub fn cross_product_matrix(x: &Vector3) -> Matrix3 { + Matrix3::new(T::zero(), -x[2], x[1], x[2], T::zero(), -x[0], -x[1], x[0], T::zero()) +} + +pub fn dump_matrix_to_file<'a, T: Scalar + Display>( + path: impl AsRef, + matrix: impl Into>, +) -> Result<(), Box> { + let file = File::create(path.as_ref())?; + let mut writer = BufWriter::new(file); + + let matrix = matrix.into(); + for i in 0..matrix.nrows() { + write!(writer, "{}", matrix[(i, 0)])?; + for j in 1..matrix.ncols() { + write!(writer, " {}", matrix[(i, j)])?; + } + writeln!(writer)?; + } + writer.flush()?; + + Ok(()) +} + +/// Dumps matrices corresponding to node-node connectivity and element-node connectivity +/// to the Matrix Market sparse storage format. +pub fn dump_mesh_connectivity_matrices( + node_path: impl AsRef, + element_path: impl AsRef, + mesh: &Mesh, +) -> Result<(), Box> +where + T: Scalar + LowerExp, + D: DimName, + C: Sync + Connectivity, + DefaultAllocator: Allocator, + Mesh: Sync, +{ + let pattern = CsrParAssembler::::default().assemble_pattern(mesh); + let nnz = pattern.nnz(); + let node_matrix = CsrMatrix::from_pattern_and_values(Arc::new(pattern), vec![1.0f64; nnz]); + + dump_csr_matrix_to_mm_file(node_path.as_ref(), &node_matrix).map_err(|err| err as Box)?; + + // Create a rectangular matrix with element index on the rows and + // node indices as columns + let mut element_node_matrix = CooMatrix::new(mesh.connectivity().len(), mesh.vertices().len()); + for (i, conn) in mesh.connectivity().iter().enumerate() { + for &j in conn.vertex_indices() { + element_node_matrix.push(i, j, 1.0f64); + } + } + + dump_csr_matrix_to_mm_file(element_path.as_ref(), &element_node_matrix.to_csr(Add::add)) + .map_err(|err| err as Box)?; + Ok(()) +} + +/// Dumps a CSR matrix to a matrix market file. +/// +/// TODO: Support writing integers etc. Probably need a custom trait for this +/// for writing the correct header, as well as for formatting numbers correctly +/// (scientific notation for floating point, integer for integers) +pub fn dump_csr_matrix_to_mm_file( + path: impl AsRef, + matrix: &CsrMatrix, +) -> Result<(), Box> { + let file = File::create(path.as_ref())?; + let mut writer = BufWriter::new(file); + + // Write header + writeln!(writer, "%%MatrixMarket matrix coordinate real general")?; + + // Write dimensions + writeln!(writer, "{} {} {}", matrix.nrows(), matrix.ncols(), matrix.nnz())?; + + for (i, j, v) in matrix.iter() { + // Indices have to be stored as 1-based + writeln!(writer, "{} {} {:.e}", i + 1, j + 1, v)?; + } + writer.flush()?; + + Ok(()) +} + +pub fn flatten_vertically_into( + output: &mut Matrix, + matrices: &[Matrix], +) where + T: Scalar, + R1: Dim, + C1: Dim, + S1: Storage, + R2: Dim, + C2: Dim, + S2: StorageMut, + ShapeConstraint: SameNumberOfColumns + SameNumberOfRows, +{ + if let Some(first) = matrices.first() { + let cols = first.ncols(); + let mut rows = 0; + + for matrix in matrices { + assert_eq!(matrix.ncols(), cols, "All matrices must have same number of columns."); + output.rows_mut(rows, matrix.nrows()).copy_from(matrix); + rows += matrix.nrows(); + } + assert_eq!( + rows, + output.nrows(), + "Number of rows in output must match number of total rows in input." + ); + } else { + assert_eq!( + output.nrows(), + 0, + "Can only vertically flatten empty slice of matrices into a matrix with 0 rows." + ); + } +} + +pub fn flatten_vertically(matrices: &[Matrix]) -> Option> +where + T: Scalar + Zero, + R: Dim, + C: Dim, + S: Storage, + ShapeConstraint: SameNumberOfRows, +{ + if let Some(first) = matrices.first() { + let rows = matrices.iter().map(Matrix::nrows).sum(); + let mut output = MatrixMN::zeros_generic(Dynamic::new(rows), first.data.shape().1); + flatten_vertically_into(&mut output, matrices); + Some(output) + } else { + None + } +} + +pub fn prefix_sum(counts: impl IntoIterator, x0: usize) -> impl Iterator { + counts.into_iter().scan(x0, |sum, x| { + let current = *sum; + *sum += x; + Some(current) + }) +} + +pub fn min_eigenvalue_symmetric(matrix: &SquareMatrix) -> T +where + T: RealField, + D: Dim + DimSub, + S: Storage, + DefaultAllocator: + Allocator + Allocator> + Allocator + Allocator>, +{ + use std::cmp::Ordering; + matrix + .symmetric_eigenvalues() + .iter() + .min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Less)) + .unwrap() + .to_owned() +} + +/// Extracts D-dimensional nodal values from a global vector using a node index list +pub fn extract_by_node_index(u: &[T], node_indices: &[usize]) -> DVector +where + T: Scalar + Copy + Zero, + D: DimName, +{ + let u = DVectorSlice::from(u); + let mut extracted = DVector::zeros(D::dim() * node_indices.len()); + for (i_local, &i_global) in node_indices.iter().enumerate() { + let ui = u.fixed_rows::(D::dim() * i_global); + extracted + .fixed_rows_mut::(D::dim() * i_local) + .copy_from(&ui); + } + extracted +} + +pub fn min_max_symmetric_eigenvalues(matrix: &SquareMatrix) -> (T, T) +where + T: RealField, + D: Dim + DimSub, + S: Storage, + DefaultAllocator: + Allocator + Allocator> + Allocator + Allocator>, +{ + use std::cmp::Ordering; + matrix + .symmetric_eigenvalues() + .iter() + .minmax_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Less)) + .into_option() + .map(|(a, b)| (*a, *b)) + .unwrap() +} + +pub fn condition_number_symmetric(matrix: &SquareMatrix) -> T +where + T: RealField, + D: Dim + DimSub, + S: Storage, + DefaultAllocator: + Allocator + Allocator> + Allocator + Allocator>, +{ + use std::cmp::Ordering; + let (min, max) = matrix + .symmetric_eigenvalues() + .into_iter() + .cloned() + .minmax_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Less)) + .into_option() + .expect("Currently don't support empty matrices"); + + max.abs() / min.abs() +} + +/* +pub fn condition_number_csr(matrix: &CsrMatrix) -> T +where + T: RealField + mkl_corrode::SupportedScalar, +{ + assert_eq!( + matrix.nrows(), + matrix.ncols(), + "Matrix must be square for condition number computation." + ); + assert!( + matrix.nrows() > 0, + "Cannot compute condition number for empty matrix." + ); + use mkl_corrode::mkl_sys::MKL_INT; + use mkl_corrode::sparse::{CsrMatrixHandle, MatrixDescription}; + use std::convert::TryFrom; + + let row_offsets: Vec<_> = matrix + .row_offsets() + .iter() + .cloned() + .map(|idx| MKL_INT::try_from(idx).unwrap()) + .collect(); + let columns: Vec<_> = matrix + .column_indices() + .iter() + .cloned() + .map(|idx| MKL_INT::try_from(idx).unwrap()) + .collect(); + + // TODO: This isn't 100% safe at the moment, because we don't properly enforce + // the necessary invariants in `Csr` (but we should, it's just a lack of time) + // TODO: Error handling + let mkl_csr = unsafe { + CsrMatrixHandle::from_raw_csr_data( + matrix.nrows(), + matrix.ncols(), + &row_offsets[..matrix.nrows()], + &row_offsets[1..], + &columns, + matrix.values(), + ) + } + .unwrap(); + + let description = MatrixDescription::default(); + + // TODO: Error handling + let eigenresult_largest = k_largest_eigenvalues(&mkl_csr, &description, 1).unwrap(); + let eigenresult_smallest = k_smallest_eigenvalues(&mkl_csr, &description, 1).unwrap(); + + let eig_max = eigenresult_largest.eigenvalues().first().unwrap(); + let eig_min = eigenresult_smallest.eigenvalues().first().unwrap(); + + eig_max.abs() / eig_min.abs() +} +*/ + +#[cfg(feature = "proptest")] +pub mod proptest { + use crate::sparse::SparsityPattern; + use crate::util::prefix_sum; + use crate::CsrMatrix; + use nalgebra::{DMatrix, Point2, Scalar, Vector2}; + use proptest::collection::{btree_set, vec}; + use proptest::prelude::*; + use proptest::strategy::ValueTree; + use proptest::test_runner::{Reason, TestRunner}; + use std::cmp::min; + use std::iter::once; + use std::sync::Arc; + + pub fn point2_f64_strategy() -> impl Strategy> { + vector2_f64_strategy().prop_map(|vector| Point2::from(vector)) + } + + pub fn vector2_f64_strategy() -> impl Strategy> { + let xrange = prop_oneof![-3.0..3.0, -100.0..100.0]; + let yrange = xrange.clone(); + (xrange, yrange).prop_map(|(x, y)| Vector2::new(x, y)) + } + + /// Simple helper function to produce square shapes for use with matrix strategies. + pub fn square_shape(dim: S) -> impl Strategy + where + S: Strategy, + { + dim.prop_map(|dim| (dim, dim)) + } + + #[derive(Debug, Clone, PartialEq)] + pub struct DMatrixStrategy { + element_strategy: ElementStrategy, + shape_strategy: ShapeStrategy, + } + + impl DMatrixStrategy<(), ()> { + pub fn new() -> Self { + Self { + element_strategy: (), + shape_strategy: (), + } + } + } + + impl DMatrixStrategy { + pub fn with_elements(self, element_strategy: E) -> DMatrixStrategy + where + E: Strategy, + { + DMatrixStrategy { + element_strategy, + shape_strategy: self.shape_strategy, + } + } + + pub fn with_shapes(self, shape_strategy: S) -> DMatrixStrategy + where + S: Strategy, + { + DMatrixStrategy { + element_strategy: self.element_strategy, + shape_strategy, + } + } + } + + impl Strategy for DMatrixStrategy + where + ElementStrategy: Clone + 'static + Strategy, + ElementStrategy::Value: Scalar, + ShapeStrategy: Clone + 'static + Strategy, + { + type Tree = Box>; + type Value = DMatrix; + + fn new_tree(&self, runner: &mut TestRunner) -> Result { + let element_strategy = self.element_strategy.clone(); + self.shape_strategy + .clone() + .prop_flat_map(move |(nrows, ncols)| { + let num_elements = nrows * ncols; + vec(element_strategy.clone(), num_elements) + .prop_map(move |elements| DMatrix::from_row_slice(nrows, ncols, &elements)) + }) + .boxed() + .new_tree(runner) + } + } + + #[derive(Debug, Clone, PartialEq)] + pub struct SparsityPatternStrategy { + shape_strategy: ShapeStrategy, + minors_per_major: MinorsPerMajorStrategy, + } + + impl SparsityPatternStrategy<(), ()> { + pub fn new() -> Self { + Self { + shape_strategy: (), + minors_per_major: (), + } + } + } + + impl SparsityPatternStrategy { + pub fn with_shapes(self, shape_strategy: S) -> SparsityPatternStrategy + where + S: Strategy, + { + SparsityPatternStrategy { + shape_strategy, + minors_per_major: self.minors_per_major, + } + } + + pub fn with_num_minors_per_major(self, strategy: N) -> SparsityPatternStrategy + where + N: Strategy, + { + SparsityPatternStrategy { + shape_strategy: self.shape_strategy, + minors_per_major: strategy, + } + } + } + + impl Strategy for SparsityPatternStrategy + where + ShapeStrategy: Clone + 'static + Strategy, + MinorsPerMajorStrategy: Clone + 'static + Strategy, + { + type Tree = Box>; + type Value = SparsityPattern; + + fn new_tree(&self, runner: &mut TestRunner) -> Result { + let shape_strategy = self.shape_strategy.clone(); + let minors_per_major = self.minors_per_major.clone(); + shape_strategy + .prop_flat_map(move |(major_dim, minor_dim)| { + // Given major_dim and minor_dim, generate a vector of counts, + // corresponding to the number of minor indices per major dimension entry + let minors_per_major = minors_per_major + .clone() + .prop_map(move |count| min(count, minor_dim)); + vec(minors_per_major, major_dim) + .prop_flat_map(move |counts| { + // Construct offsets from counts + let offsets = prefix_sum(counts.iter().cloned().chain(once(0)), 0).collect::>(); + + // We build one strategy per major entry (i.e. per row in a CSR matrix) + let mut major_strategies = Vec::with_capacity(major_dim); + for count in counts { + if 10 * count <= minor_dim { + // If we require less than approx. 10% of minor_dim, + // every pick is at least 90% likely to not be an index + // we already picked, so we can generate a set + major_strategies.push( + btree_set(0..minor_dim, count) + .prop_map(|indices| indices.into_iter().collect::>()) + .boxed(), + ) + } else { + // Otherwise, we simply shuffle the integers + // [0, minor_dim) and take the `count` first + let strategy = Just((0..minor_dim).collect::>()) + .prop_shuffle() + .prop_map(move |mut indices| { + let indices = &mut indices[0..count]; + indices.sort_unstable(); + indices.to_vec() + }) + .boxed(); + major_strategies.push(strategy); + } + } + (Just(major_dim), Just(minor_dim), Just(offsets), major_strategies) + }) + .prop_map(move |(major_dim, minor_dim, offsets, minor_indices_by_major)| { + let minor_indices: Vec = minor_indices_by_major.into_iter().flatten().collect(); + SparsityPattern::from_offsets_and_indices(major_dim, minor_dim, offsets, minor_indices) + }) + }) + .boxed() + .new_tree(runner) + } + } + + #[derive(Debug, Clone, PartialEq)] + pub struct CsrStrategy { + pattern_strategy: SparsityPatternStrategy, + element_strategy: ElementStrategy, + } + + impl CsrStrategy<(), (), ()> { + pub fn new() -> Self { + Self { + pattern_strategy: SparsityPatternStrategy::new(), + element_strategy: (), + } + } + } + + impl + CsrStrategy + { + pub fn with_elements(self, element_strategy: E) -> CsrStrategy + where + E: Strategy, + { + CsrStrategy { + pattern_strategy: self.pattern_strategy, + element_strategy, + } + } + + pub fn with_shapes(self, shape_strategy: S) -> CsrStrategy + where + S: Strategy, + { + let pattern = self.pattern_strategy.with_shapes(shape_strategy); + CsrStrategy { + pattern_strategy: pattern, + element_strategy: self.element_strategy, + } + } + + pub fn with_cols_per_row(self, cols_per_row_strategy: N) -> CsrStrategy + where + N: Strategy, + { + let pattern = self + .pattern_strategy + .with_num_minors_per_major(cols_per_row_strategy); + CsrStrategy { + pattern_strategy: pattern, + element_strategy: self.element_strategy, + } + } + } + + impl Strategy + for CsrStrategy + where + ElementStrategy: Clone + 'static + Strategy, + ShapeStrategy: Clone + 'static + Strategy, + MinorsPerMajorStrategy: Clone + 'static + Strategy, + { + type Tree = Box>; + type Value = CsrMatrix; + + fn new_tree(&self, runner: &mut TestRunner) -> Result { + let element_strategy = self.element_strategy.clone(); + let pattern_strategy = self.pattern_strategy.clone(); + pattern_strategy + .prop_flat_map(move |pattern| { + let nnz = pattern.nnz(); + (Just(pattern), vec(element_strategy.clone(), nnz)) + }) + .prop_map(|(pattern, values)| CsrMatrix::from_pattern_and_values(Arc::new(pattern), values)) + .boxed() + .new_tree(runner) + } + } + + #[cfg(test)] + mod tests { + use super::{CsrStrategy, DMatrixStrategy, SparsityPatternStrategy}; + use itertools::Itertools; + use proptest::prelude::*; + + proptest! { + #[test] + fn dmatrix_strategy_respects_strategies( + matrix in DMatrixStrategy::new() + .with_shapes((Just(5), 2usize..=3)) + .with_elements(0i32 ..= 5)) + { + prop_assert_eq!(matrix.nrows(), 5); + prop_assert!(matrix.ncols() >= 2); + prop_assert!(matrix.ncols() <= 3); + prop_assert!(matrix.iter().cloned().all(|x| x >= 0 && x <= 5)); + } + + #[test] + fn sparsity_pattern_strategy_respects_strategies( + pattern in SparsityPatternStrategy::new() + .with_shapes((Just(5), 2usize..=3)) + .with_num_minors_per_major(1usize ..= 2)) + { + prop_assert_eq!(pattern.major_dim(), 5); + prop_assert!(pattern.minor_dim() >= 2); + prop_assert!(pattern.minor_dim() <= 3); + + let counts: Vec<_> = pattern.major_offsets() + .iter() + .tuple_windows() + .map(|(prev, next)| next - prev) + .collect(); + + prop_assert!(counts.iter().cloned().all(|c| c >= 1 && c <= 2)); + } + + #[test] + fn csr_strategy_respects_strategies( + matrix in CsrStrategy::new() + .with_shapes((Just(5), 2usize..=3)) + .with_cols_per_row(1usize..=2) + .with_elements(0i32..5)) + { + prop_assert_eq!(matrix.nrows(), 5); + prop_assert!(matrix.ncols() >= 2); + prop_assert!(matrix.ncols() <= 3); + prop_assert!(matrix.values().iter().cloned().all(|x| x >= 0 && x <= 5)); + + let counts: Vec<_> = matrix.row_offsets() + .iter() + .tuple_windows() + .map(|(prev, next)| next - prev) + .collect(); + + prop_assert!(counts.iter().cloned().all(|c| c >= 1 && c <= 2)); + } + } + } +} diff --git a/fenris/tests/integration.rs b/fenris/tests/integration.rs new file mode 100644 index 0000000..f490ac7 --- /dev/null +++ b/fenris/tests/integration.rs @@ -0,0 +1,4 @@ +mod integration_tests; + +#[macro_use] +pub mod utils; diff --git a/fenris/tests/integration_tests/assembly.rs b/fenris/tests/integration_tests/assembly.rs new file mode 100644 index 0000000..5030ad2 --- /dev/null +++ b/fenris/tests/integration_tests/assembly.rs @@ -0,0 +1,235 @@ +use fenris::geometry::polymesh::PolyMesh3d; +use fenris::geometry::proptest_strategies::rectangular_uniform_mesh_strategy; +use fenris::model::{Quad4Model, Tet4Model, Tri3d2Model}; +use fenris::quadrature::{quad_quadrature_strength_5_f64, tet_quadrature_strength_5, tri_quadrature_strength_5_f64}; +use fenris::solid::materials::{LinearElasticMaterial, YoungPoisson}; +use fenris::solid::{ElasticityModel, ElasticityModelParallel}; +use hamilton2::calculus::{approximate_jacobian, VectorFunctionBuilder}; +use nalgebra::{DVector, DVectorSlice, DVectorSliceMut}; + +use fenris::geometry::procedural::{create_rectangular_uniform_hex_mesh, create_unit_square_uniform_quad_mesh_2d}; +use fenris::mesh::Tet4Mesh; +use proptest::prelude::*; +use std::convert::TryFrom; +use std::ops::Add; + +use crate::assert_approx_matrix_eq; + +#[test] +fn tet4_stiffness_matrix_is_negative_derivative_of_forces() { + // TODO: Make a property-based test out of this rather than choosing a fixed mesh + let mesh = create_rectangular_uniform_hex_mesh(1.0, 2, 2, 2, 1); + let mesh = Tet4Mesh::try_from(&PolyMesh3d::from(&mesh).triangulate().unwrap()).unwrap(); + + let h = 1e-6; + + let lame = YoungPoisson { + young: 1e6, + poisson: 0.2, + }; + let material = LinearElasticMaterial::from(lame); + let quadrature = tet_quadrature_strength_5(); + let model = Tet4Model::from_mesh_and_quadrature(mesh.clone(), quadrature); + + let u = DVector::zeros(model.ndof()); + let a = model.assemble_stiffness(&u, &material).build_dense(); + + let func = VectorFunctionBuilder::with_dimension(model.ndof()).with_function(move |f, u| { + f.copy_from(&model.assemble_elastic_pseudo_forces(*u, &material)); + }); + + let a_approx = -approximate_jacobian(func, &u, &h); + let diff = &a - &a_approx; + + let approx_equals = diff.norm() / (a.norm() + a_approx.norm()) < 1e-5; + assert!(approx_equals); +} + +#[test] +fn tri3d2_stiffness_matrix_csr_and_coo_assembly_agree() { + // let mesh = create_rectangular_uniform_hex_mesh(1.0, 2, 2, 2, 1); + let mesh = create_unit_square_uniform_quad_mesh_2d(3).split_into_triangles(); + + let lame = YoungPoisson { + young: 1e6, + poisson: 0.2, + }; + let material = LinearElasticMaterial::from(lame); + let quadrature = tri_quadrature_strength_5_f64(); + let model = Tri3d2Model::from_mesh_and_quadrature(mesh.clone(), quadrature); + + let u = DVector::zeros(model.ndof()); + let a_coo_csr = model.assemble_stiffness(&u, &material).to_csr(Add::add); + + let mut a_csr = a_coo_csr.clone(); + a_csr.transform_values(|_, _, val| *val = 0.0); + model.assemble_stiffness_into(&mut a_csr, &u, &material); + + assert_eq!(a_coo_csr.nnz(), a_csr.nnz()); + + let a_coo_csr_dense = a_coo_csr.build_dense(); + let abstol = 1e-14 * a_coo_csr_dense.abs().max(); + + assert_approx_matrix_eq!(&a_coo_csr_dense, &a_csr.build_dense(), abstol = abstol); +} + +#[test] +fn tet4_stiffness_matrix_csr_and_coo_assembly_agree() { + // let mesh = create_rectangular_uniform_hex_mesh(1.0, 2, 2, 2, 1); + let mesh = create_rectangular_uniform_hex_mesh(1.0, 2, 2, 2, 1); + let mesh = Tet4Mesh::try_from(&PolyMesh3d::from(&mesh).triangulate().unwrap()).unwrap(); + + let lame = YoungPoisson { + young: 1e6, + poisson: 0.2, + }; + let material = LinearElasticMaterial::from(lame); + let quadrature = tet_quadrature_strength_5(); + let model = Tet4Model::from_mesh_and_quadrature(mesh.clone(), quadrature); + + let u = DVector::zeros(model.ndof()); + let a_coo_csr = model.assemble_stiffness(&u, &material).to_csr(Add::add); + + let mut a_csr = a_coo_csr.clone(); + a_csr.transform_values(|_, _, val| *val = 0.0); + model.assemble_stiffness_into(&mut a_csr, &u, &material); + + assert_eq!(a_coo_csr.nnz(), a_csr.nnz()); + + let a_coo_csr_dense = a_coo_csr.build_dense(); + let abstol = 1e-14 * a_coo_csr_dense.abs().max(); + + assert_approx_matrix_eq!(&a_coo_csr_dense, &a_csr.build_dense(), abstol = abstol); +} + +#[test] +fn tet4_elastic_forces_sequential_and_parallel_agree() { + // let mesh = create_rectangular_uniform_hex_mesh(1.0, 2, 2, 2, 1); + let mesh = create_rectangular_uniform_hex_mesh(1.0, 2, 2, 2, 1); + let mesh = Tet4Mesh::try_from(&PolyMesh3d::from(&mesh).triangulate().unwrap()).unwrap(); + + let lame = YoungPoisson { + young: 1e6, + poisson: 0.2, + }; + let material = LinearElasticMaterial::from(lame); + let quadrature = tet_quadrature_strength_5(); + let model = Tet4Model::from_mesh_and_quadrature(mesh.clone(), quadrature); + let u = DVector::from_iterator(model.ndof(), (0..10).map(|i| i as f64).cycle().take(model.ndof())); + + let f_seq = model.assemble_elastic_pseudo_forces(DVectorSlice::from(&u), &material); + + let mut f_par = DVector::zeros(model.ndof()); + model.assemble_elastic_pseudo_forces_into_par(DVectorSliceMut::from(&mut f_par), DVectorSlice::from(&u), &material); + + assert_eq!(f_seq.len(), f_par.len()); + + let abstol = 1e-14 * f_seq.abs().max(); + assert_approx_matrix_eq!(&f_seq, &f_par, abstol = abstol); +} + +#[test] +fn tet4_stiffness_matrix_csr_and_csr_par_agree() { + // let mesh = create_rectangular_uniform_hex_mesh(1.0, 2, 2, 2, 1); + let mesh = create_rectangular_uniform_hex_mesh(1.0, 2, 2, 2, 1); + let mesh = Tet4Mesh::try_from(&PolyMesh3d::from(&mesh).triangulate().unwrap()).unwrap(); + + let lame = YoungPoisson { + young: 1e6, + poisson: 0.2, + }; + let material = LinearElasticMaterial::from(lame); + let quadrature = tet_quadrature_strength_5(); + let model = Tet4Model::from_mesh_and_quadrature(mesh.clone(), quadrature); + + let u = DVector::zeros(model.ndof()); + // Construct CSR matrix with correct pattern + // TODO: Implement direct construction of pattern + let a_coo_csr = model.assemble_stiffness(&u, &material).to_csr(Add::add); + + let a_csr = { + let mut a_csr = a_coo_csr.clone(); + a_csr.transform_values(|_, _, val| *val = 0.0); + model.assemble_stiffness_into(&mut a_csr, &u, &material); + a_csr + }; + + let a_csr_par = { + let mut a_csr_par = a_coo_csr.clone(); + a_csr_par.transform_values(|_, _, val| *val = 0.0); + model.assemble_stiffness_into_par(&mut a_csr_par, &u, &material); + a_csr_par + }; + + assert_eq!(a_csr.nnz(), a_csr_par.nnz()); + + let a_csr_dense = a_csr.build_dense(); + let abstol = 1e-14 * dbg!(a_csr_dense.abs().max()); + + assert_approx_matrix_eq!(&a_csr_dense, &a_csr_par.build_dense(), abstol = abstol); +} + +proptest! { + #[test] + fn stiffness_matrix_is_negative_derivative_of_forces_for_rectangular_grids_with_linear_material_bilinear_quad( + mesh in rectangular_uniform_mesh_strategy(1.0, 4)) { + prop_assume!(mesh.connectivity().len() > 0); + + let h = 1e-6; + + let lame = YoungPoisson { + young: 1e6, + poisson: 0.2, + }; + let material = LinearElasticMaterial::from(lame); + let quadrature = quad_quadrature_strength_5_f64(); + let model = Quad4Model::from_mesh_and_quadrature(mesh.clone(), quadrature); + + let u = DVector::zeros(model.ndof()); + let a = model.assemble_stiffness(&u, &material).build_dense(); + + let func = VectorFunctionBuilder::with_dimension(model.ndof()) + .with_function(move |f, u| { + f.copy_from(&model.assemble_elastic_pseudo_forces(*u, &material)); + }); + + let a_approx = - approximate_jacobian(func, &u, &h); + let diff = &a - &a_approx; + + let approx_equals = diff.norm() / (a.norm() + a_approx.norm()) < 1e-5; + prop_assert!(approx_equals); + } + + #[test] + fn stiffness_matrix_is_negative_derivative_of_forces_for_rectangular_domain_with_linear_material_linear_tri( + mesh in rectangular_uniform_mesh_strategy(1.0, 4)) { + let mesh = mesh.split_into_triangles(); + prop_assume!(mesh.connectivity().len() > 0); + + let h = 1e-6; + + let lame = YoungPoisson { + young: 1e6, + poisson: 0.2, + }; + let material = LinearElasticMaterial::from(lame); + let quadrature = tri_quadrature_strength_5_f64(); + let model = Tri3d2Model::from_mesh_and_quadrature(mesh.clone(), quadrature); + + let u = DVector::zeros(model.ndof()); + let a = model.assemble_stiffness(&u, &material).build_dense(); + + let func = VectorFunctionBuilder::with_dimension(model.ndof()) + .with_function(move |f, u| { + f.copy_from(&model.assemble_elastic_pseudo_forces(*u, &material)); + }); + + let a_approx = - approximate_jacobian(func, &u, &h); + let diff = &a - &a_approx; + + let approx_equals = diff.norm() / (a.norm() + a_approx.norm()) < 1e-5; + prop_assert!(approx_equals); + } + + // TODO: Need way more tests for linear tri elements! +} diff --git a/fenris/tests/integration_tests/cg_fem.rs b/fenris/tests/integration_tests/cg_fem.rs new file mode 100644 index 0000000..9728f53 --- /dev/null +++ b/fenris/tests/integration_tests/cg_fem.rs @@ -0,0 +1,95 @@ +//! Tests for CG applied to finite element matrices. + +use crate::assert_approx_matrix_eq; +use fenris::cg::{CgWorkspace, ConjugateGradient, RelativeResidualCriterion}; +use fenris::geometry::procedural::create_rectangular_uniform_hex_mesh; +use fenris::model::NodalModel3d; +use fenris::nalgebra::DVector; +use fenris::quadrature::hex_quadrature_strength_5; +use fenris::solid::materials::{LinearElasticMaterial, YoungPoisson}; +use fenris::solid::ElasticityModel; +use fenris::CsrMatrix; +use std::ops::Add; + +#[test] +fn cg_linear_elasticity_dynamic_regular_grid() { + let mesh = create_rectangular_uniform_hex_mesh(1.0f64, 1, 1, 1, 5); + let model = NodalModel3d::from_mesh_and_quadrature(mesh, hex_quadrature_strength_5()); + + // Use linear elastic material since it is guaranteed to give positive semi-definite + // stiffness matrices + let material = LinearElasticMaterial::from(YoungPoisson { + young: 1e4, + poisson: 0.48, + }); + + let u = DVector::zeros(model.ndof()); + let mass = model.assemble_mass(1.0).to_csr(Add::add); + let mut stiffness = CsrMatrix::from_pattern_and_values(mass.sparsity_pattern(), vec![0.0; mass.nnz()]); + model.assemble_stiffness_into(&mut stiffness, &u, &material); + + let dt = 0.1; + let system_matrix = mass + stiffness * (dt * dt); + + let solutions = vec![ + DVector::repeat(model.ndof(), 1.0), + DVector::repeat(model.ndof(), 2.0), + DVector::repeat(model.ndof(), 3.0), + ]; + + // Use a workspace and solve several systems to test that the internal state of the workspace + // does not impact the solutions + let mut cg_workspace = CgWorkspace::default(); + + for x0 in solutions { + let b = &system_matrix * &x0; + + let mut x_workspace = DVector::repeat(model.ndof(), 0.0); + let output_workspace = ConjugateGradient::with_workspace(&mut cg_workspace) + .with_operator(&system_matrix) + .with_stopping_criterion(RelativeResidualCriterion::new(1e-8)) + .solve_with_guess(&b, &mut x_workspace) + .unwrap(); + + let mut x_no_workspace = DVector::repeat(model.ndof(), 0.0); + let output_no_workspace = ConjugateGradient::new() + .with_operator(&system_matrix) + .with_stopping_criterion(RelativeResidualCriterion::new(1e-8)) + .solve_with_guess(&b, &mut x_no_workspace) + .unwrap(); + + // Results should be exactly the same regardless of whether we use a workspace or not. + assert_eq!(x_no_workspace, x_workspace); + assert_eq!(output_workspace.num_iterations, output_no_workspace.num_iterations); + let x = x_no_workspace; + assert_approx_matrix_eq!(&x, &x0, abstol = 1e-7); + + println!( + "Matrix size: {}x{} ({} nnz)", + system_matrix.nrows(), + system_matrix.ncols(), + system_matrix.nnz() + ); + println!("Num CG iterations: {}", output_no_workspace.num_iterations); + + let system_diagonal = DVector::from_iterator(model.ndof(), system_matrix.diag_iter().map(|d_i| d_i.recip())); + let diagonal_preconditioner = CsrMatrix::from_diagonal(&system_diagonal); + let mut x = DVector::repeat(model.ndof(), 0.0); + let output_preconditioned = ConjugateGradient::new() + .with_operator(&system_matrix) + .with_preconditioner(&diagonal_preconditioner) + .with_stopping_criterion(RelativeResidualCriterion::new(1e-8)) + .solve_with_guess(&b, &mut x) + .unwrap(); + assert_approx_matrix_eq!(&x, &x0, abstol = 1e-7); + + println!( + "Num CG iterations (diagonal precond): {}", + output_preconditioned.num_iterations + ); + + // For these particular matrices we expect the diagonal preconditioner to + // yield less iterations + assert!(output_preconditioned.num_iterations < output_workspace.num_iterations); + } +} diff --git a/fenris/tests/integration_tests/embedding.rs b/fenris/tests/integration_tests/embedding.rs new file mode 100644 index 0000000..c12a8ef --- /dev/null +++ b/fenris/tests/integration_tests/embedding.rs @@ -0,0 +1,139 @@ +use fenris::element::{Hex8Element, Quad4d2Element}; +use fenris::embedding::{ + compute_element_embedded_quadrature, construct_embedded_quadrature_for_element_2d, embed_cell_2d, + optimize_quadrature, QuadratureOptions, +}; +use fenris::geometry::procedural::{approximate_triangle_mesh_for_sdf_2d, create_simple_stupid_sphere}; +use fenris::geometry::sdf::SdfCircle; +use fenris::geometry::{ConvexPolygon, Hexahedron, Quad2d}; +use fenris::lp_solvers::GlopSolver; +use fenris::quadrature::{tet_quadrature_strength_10, tri_quadrature_strength_11, Quadrature}; +use matrixcompare::assert_scalar_eq; +use nalgebra::{Point2, Point3, Vector2, Vector3}; + +use std::convert::TryFrom; + +#[test] +fn optimize_quadrature_2d() { + let quad = Quad2d([ + Point2::new(0.0, 2.0), + Point2::new(2.0, 2.0), + Point2::new(2.0, 3.0), + Point2::new(0.0, 3.0), + ]); + + let circle = SdfCircle { + radius: 1.0, + center: Vector2::new(2.0, 3.0), + }; + let quad_polygon = ConvexPolygon::try_from(quad).unwrap(); + + let element = Quad4d2Element::from(quad); + let triangle_mesh = approximate_triangle_mesh_for_sdf_2d(&circle, 0.05); + let embedded_polygons = embed_cell_2d(&quad_polygon, &triangle_mesh).unwrap(); + let quadrature = + construct_embedded_quadrature_for_element_2d(&element, &embedded_polygons, tri_quadrature_strength_11()); + let quadrature_opt = optimize_quadrature(&quadrature, 9, &GlopSolver::new()).unwrap(); + + println!( + "Num quadrature points before optimization: {}", + quadrature.points().len() + ); + println!( + "Num quadrature points after optimization: {}", + quadrature_opt.points().len() + ); + + let f = |x: f64, y: f64| { + -1.0 * x.powi(5) * y.powi(4) + 2.0 * x * y - 5.0 * x.powi(4) * y.powi(5) + + 2.5 * x.powi(1) * y.powi(8) + + x * y + + 2.0 + }; + + let f = |p: &Vector2| f(p.x, p.y); + + let original_integral: f64 = quadrature.integrate(f); + let optimized_integral: f64 = quadrature_opt.integrate(f); + + assert_scalar_eq!(original_integral, optimized_integral, comp = abs, tol = 1e-9); +} + +#[test] +fn optimize_quadrature_3d() { + let element = Hex8Element::from_vertices([ + Point3::new(0.0, 2.0, 0.0), + Point3::new(2.0, 2.0, 0.0), + Point3::new(2.0, 3.0, 0.0), + Point3::new(0.0, 3.0, 0.0), + Point3::new(0.0, 2.0, 2.0), + Point3::new(2.0, 2.0, 2.0), + Point3::new(2.0, 3.0, 2.0), + Point3::new(0.0, 3.0, 2.0), + ]); + let hex = Hexahedron::from_vertices(element.vertices().clone()); + let embedded_mesh = + create_simple_stupid_sphere(&Point3::new(2.0, 3.0, 2.0), 1.0, 5).intersect_convex_polyhedron(&hex); + let quadrature = compute_element_embedded_quadrature( + &element, + &embedded_mesh, + tet_quadrature_strength_10(), + &QuadratureOptions::default(), + ) + .unwrap(); + let quadrature_opt = optimize_quadrature(&quadrature, 8, &GlopSolver::new()).unwrap(); + + println!( + "Num quadrature points before optimization: {}", + quadrature.points().len() + ); + println!( + "Num quadrature points after optimization: {}", + quadrature_opt.points().len() + ); + + let f = |x: f64, y: f64, z: f64| { + -1.0 * x.powi(4) * y.powi(4) + 2.0 * x * y * z.powi(3) - 5.0 * x.powi(4) * y.powi(4) + + 2.5 * x.powi(1) * y.powi(7) + + 3.5 * z.powi(8) + - 9.0 * x.powi(3) * y.powi(3) * z.powi(2) + + z + + x * y + + 2.0 + }; + + let f = |p: &Vector3| f(p.x, p.y, p.z); + + let original_integral: f64 = quadrature.integrate(f); + let optimized_integral: f64 = quadrature_opt.integrate(f); + + assert_scalar_eq!(original_integral, optimized_integral, comp = abs, tol = 1e-9); +} + +#[test] +fn zeroth_order_simplification() { + // Points are arbitrary + let points = vec![ + Vector3::new(-0.5, 0.3, 0.5), + Vector3::new(0.3, 0.2, -0.2), + Vector3::new(0.4, 0.1, -0.5), + Vector3::new(0.5, -0.2, 0.1), + ]; + let weights = vec![0.1, 0.5, 0.8, 3.0]; + let quadrature = (weights.clone(), points.clone()); + let quadrature_opt = optimize_quadrature(&quadrature, 0, &GlopSolver::new()).unwrap(); + let opt_weights = quadrature_opt.0; + let opt_points = quadrature_opt.1; + + assert_eq!(opt_weights.len(), 1); + assert_eq!(opt_points.len(), 1); + let expected_weight: f64 = weights.iter().sum(); + assert_scalar_eq!(opt_weights[0], expected_weight, comp = abs, tol = 1e-9); + + assert!( + opt_points[0] == points[0] + || opt_points[0] == points[1] + || opt_points[0] == points[2] + || opt_points[0] == points[3] + ) +} diff --git a/fenris/tests/integration_tests/mod.rs b/fenris/tests/integration_tests/mod.rs new file mode 100644 index 0000000..8b7213d --- /dev/null +++ b/fenris/tests/integration_tests/mod.rs @@ -0,0 +1,3 @@ +mod assembly; +mod cg_fem; +mod embedding; diff --git a/fenris/tests/unit.rs b/fenris/tests/unit.rs new file mode 100644 index 0000000..5e4b33c --- /dev/null +++ b/fenris/tests/unit.rs @@ -0,0 +1,4 @@ +mod unit_tests; + +#[macro_use] +pub mod utils; diff --git a/fenris/tests/unit_tests/assembly.proptest-regressions b/fenris/tests/unit_tests/assembly.proptest-regressions new file mode 100644 index 0000000..149863c --- /dev/null +++ b/fenris/tests/unit_tests/assembly.proptest-regressions @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 3bf0a1bc67c2ee2b1c4d5e4b432a87cd194d9ff40a125f2882609ba13849b561 # shrinks to mesh = Mesh2d { vertices: [Matrix { data: [0.0, 0.0] }, Matrix { data: [1.0, 0.0] }, Matrix { data: [0.0, -1.0] }, Matrix { data: [1.0, -1.0] }], cells: [QuadIndices([2, 3, 1, 0])] } diff --git a/fenris/tests/unit_tests/assembly.rs b/fenris/tests/unit_tests/assembly.rs new file mode 100644 index 0000000..eb7f8b5 --- /dev/null +++ b/fenris/tests/unit_tests/assembly.rs @@ -0,0 +1,621 @@ +use fenris::assembly::{ + apply_homogeneous_dirichlet_bc_csr, apply_homogeneous_dirichlet_bc_matrix, + assemble_generalized_element_elliptic_term, assemble_generalized_element_mass, + assemble_generalized_element_stiffness, CsrParAssembler, ElementConnectivityAssembler, +}; +use fenris::geometry::Quad2d; +use fenris::model::{Quad4Model, Tet4Model}; +use fenris::quadrature::{quad_quadrature_strength_5_f64, tet_quadrature_strength_5, Quadrature}; +use fenris::solid::assembly::MaterialEllipticOperator; +use fenris::solid::materials::{LameParameters, LinearElasticMaterial, StVKMaterial, YoungPoisson}; +use fenris::solid::ElasticMaterialModel; +use fenris::solid::ElasticityModel; +use fenris::sparse::{CsrMatrix, SparsityPattern}; +use hamilton2::calculus::{approximate_jacobian, VectorFunction, VectorFunctionBuilder}; +use nalgebra::dimension::U8; +use nalgebra::{DMatrix, DVector, DefaultAllocator, DimMin, DimName, DimNameMul, VectorN}; +use nalgebra::{Matrix2, Matrix2x4, Matrix4, MatrixN, Point2, Point3, RealField, U2}; +use num::{FromPrimitive, Zero}; +use std::iter::once; + +use fenris::allocators::ElementConnectivityAllocator; +use fenris::connectivity::{Quad4d2Connectivity, Tet4Connectivity}; +use fenris::element::{ElementConnectivity, Quad4d2Element, Tet4Element}; +use fenris::geometry::polymesh::PolyMesh3d; +use fenris::geometry::procedural::{create_rectangular_uniform_hex_mesh, create_unit_square_uniform_quad_mesh_2d}; +use fenris::mesh::Tet4Mesh; +use std::convert::TryFrom; + +#[derive(Debug, Copy, Clone)] +struct MockIdentityMaterial; + +impl ElasticMaterialModel for MockIdentityMaterial +where + T: RealField, +{ + fn compute_strain_energy_density(&self, _deformation_gradient: &Matrix2) -> T { + unimplemented!() + } + + fn compute_stress_tensor(&self, _deformation_gradient: &Matrix2) -> Matrix2 { + Matrix2::identity() + } + + fn contract_stress_tensor_with( + &self, + _deformation_gradient: &Matrix2, + _a: &VectorN, + _b: &VectorN, + ) -> Matrix2 { + Matrix2::zero() + } +} + +#[derive(Debug, Copy, Clone)] +struct MockSimpleMaterial; + +#[allow(non_snake_case)] +impl ElasticMaterialModel for MockSimpleMaterial +where + T: RealField, +{ + fn compute_strain_energy_density(&self, _deformation_gradient: &Matrix2) -> T { + unimplemented!() + } + + fn compute_stress_tensor(&self, F: &Matrix2) -> Matrix2 { + F - Matrix2::identity() + } + + fn contract_stress_tensor_with(&self, _F: &Matrix2, a: &VectorN, b: &VectorN) -> Matrix2 { + Matrix2::identity() * a.dot(&b) + } +} + +fn reference_quad() -> Quad2d +where + T: RealField, +{ + Quad2d([ + Point2::new(-T::one(), -T::one()), + Point2::new(T::one(), -T::one()), + Point2::new(T::one(), T::one()), + Point2::new(-T::one(), T::one()), + ]) +} + +#[test] +fn analytic_comparison_of_element_mass_matrix_for_reference_element() { + let density = 3.0; + + let quadrature = quad_quadrature_strength_5_f64(); + let quad = Quad4d2Element::from(reference_quad()); + + let m = assemble_generalized_element_mass::<_, U2, _, _>(&quad, density, &quadrature); + + #[rustfmt::skip] + let expected4x4 = (density / 9.0) * Matrix4::new( + 4.0, 2.0, 1.0, 2.0, + 2.0, 4.0, 2.0, 1.0, + 1.0, 2.0, 4.0, 2.0, + 2.0, 1.0, 2.0, 4.0); + + let mut expected8x8: MatrixN = MatrixN::zero(); + expected8x8 + .slice_with_steps_mut((0, 0), (4, 4), (1, 1)) + .copy_from(&expected4x4); + expected8x8 + .slice_with_steps_mut((1, 1), (4, 4), (1, 1)) + .copy_from(&expected4x4); + + let diff = m - expected8x8; + assert!(diff.norm() <= 1e-6); +} + +#[test] +fn quad4d2_constant_displacement_gives_zero_elastic_forces_for_reference_element() { + let lame = fenris::solid::materials::LameParameters { mu: 2.0, lambda: 3.0 }; + let material = fenris::solid::materials::LinearElasticMaterial::from(lame); + + let u = 3.0 * Matrix2x4::repeat(1.0); + + let quadrature = quad_quadrature_strength_5_f64(); + let quad = Quad4d2Element::from(reference_quad()); + + let elliptic_operator = MaterialEllipticOperator(&material); + let f_e = assemble_generalized_element_elliptic_term(&quad, &elliptic_operator, &u, &quadrature); + assert!(f_e.norm() < 1e-14); +} + +#[test] +fn quad4d2_constant_displacement_gives_zero_elastic_forces_for_arbitrary_quad() { + let lame = fenris::solid::materials::LameParameters { mu: 2.0, lambda: 3.0 }; + let material = fenris::solid::materials::LinearElasticMaterial::from(lame); + let u = 3.0 * Matrix2x4::repeat(1.0); + + let quadrature = quad_quadrature_strength_5_f64(); + let quad = Quad4d2Element::from_vertices([ + Point2::new(-2.0, -3.0), + Point2::new(1.0, -1.0), + Point2::new(2.0, 4.0), + Point2::new(-1.0, 3.0), + ]); + + let elliptic_operator = MaterialEllipticOperator(&material); + let f_e = assemble_generalized_element_elliptic_term(&quad, &elliptic_operator, &u, &quadrature); + assert!(f_e.norm() < 1e-14); +} + +#[test] +fn analytic_comparison_of_element_elastic_force_for_reference_element() { + let u = 3.0 * Matrix2x4::repeat(1.0); + let quadrature = quad_quadrature_strength_5_f64(); + let material = MockIdentityMaterial; + let quad = Quad4d2Element::from(reference_quad()); + + let elliptic_operator = MaterialEllipticOperator(&material); + let f_e = assemble_generalized_element_elliptic_term(&quad, &elliptic_operator, &u, &quadrature); + #[rustfmt::skip] + let expected = Matrix2x4::new(1.0, -1.0, -1.0, 1.0, + 1.0, 1.0, -1.0, -1.0); + let diff = f_e - expected; + assert!(diff.norm() < 1e-14); +} + +// TODO: Test elastic forces for arbitrary element + +#[test] +fn analytic_comparison_of_element_stiffness_matrix_for_reference_element() { + let u = 3.0 * Matrix2x4::repeat(1.0); + let material = MockSimpleMaterial; + let quadrature = quad_quadrature_strength_5_f64(); + let quad = Quad4d2Element::from(reference_quad()); + + let elliptic_operator = MaterialEllipticOperator(&material); + let a = assemble_generalized_element_stiffness(&quad, &elliptic_operator, &u, &quadrature); + + // For the given mock material, the contraction yields tr(B) I, + // and so the integral over the element K reads + // A^K_IJ = int_K tr(B_IJ) * I dX + // with tr(B_IJ) = grad phi_I dot grad phi_J + // in other words, the 2x2 matrix A^K_IJ corresponds to + // the value of the *scalar* Laplacian stiffness matrix for basis + // functions IJ multiplied by the 2x2 identity matrix. + #[rustfmt::skip] + let expected4x4 = Matrix4::new( 2.0/3.0, -1.0/6.0, -1.0/3.0, -1.0/6.0, + -1.0/6.0, 2.0/3.0, -1.0/6.0, -1.0/3.0, + -1.0/3.0, -1.0/6.0, 2.0/3.0, -1.0/6.0, + -1.0/6.0, -1.0/3.0, -1.0/6.0, 2.0/3.0); + let mut expected8x8: MatrixN = MatrixN::zero(); + expected8x8 + .slice_with_steps_mut((0, 0), (4, 4), (1, 1)) + .copy_from(&expected4x4); + expected8x8 + .slice_with_steps_mut((1, 1), (4, 4), (1, 1)) + .copy_from(&expected4x4); + + let diff = a - expected8x8; + assert!(diff.norm() <= 1e-6); +} + +#[test] +#[allow(non_snake_case)] +fn quad4d2_mass_matrix_vector_product_with_ones() { + // It can be shown that, assuming the transformation from the reference + // element to each individual element K is a linear transformation, + // then + // (M * 1)_Ii = rho_0 * sum_{K in S_I} |det J_K| + // where S_I = { K | intersection of K and support of basis function I is non-empty}, + // rho_0 is the rest density and J_K is the Jacobian of the (linear) + // transformation from the reference element to each element K. + // + // If furthermore all cells have the same size, we must only compute + // |det J| once and scale it by the number of elements the node appears in. + + let resolutions = [1, 2, 3, 4, 8, 9, 11]; + let quadrature = quad_quadrature_strength_5_f64(); + + for resolution in &resolutions { + let mesh = create_unit_square_uniform_quad_mesh_2d(*resolution); + let ndof = 2 * mesh.vertices().len(); + let model = Quad4Model::from_mesh_and_quadrature(mesh, quadrature.clone()); + let rho_0 = 3.0; + + let mass_matrix = model.assemble_mass(rho_0).build_dense(); + + // Cells all have the same size + let cell_size = 1.0 / f64::from_usize(*resolution).unwrap(); + let abs_det_J = cell_size * cell_size / 4.0; + let num_nodes = model.vertices().len(); + + let mut node_counts = vec![0u32; num_nodes]; + for connectivity in model.connectivity() { + for node_index in &connectivity.0 { + node_counts[*node_index] += 1; + } + } + + let expected_values = node_counts + .iter() + .map(|i| rho_0 * abs_det_J * f64::from(*i)) + .flat_map(|v| once(v).chain(once(v))); + let expected = DVector::from_iterator(ndof, expected_values); + + let result = mass_matrix * DVector::repeat(ndof, 1.0); + let diff = result - expected; + + assert!(diff.norm() < ndof as f64 * 1e-12); + } +} + +#[test] +#[allow(non_snake_case)] +fn tet4_mass_matrix_vector_product_with_ones() { + // See the comment above for the explanation for this test + let resolutions = [1, 2, 3, 4, 8]; + let quadrature = tet_quadrature_strength_5(); + + for resolution in &resolutions { + let mesh = create_rectangular_uniform_hex_mesh(1.0, 1, 1, 1, *resolution); + let mesh = Tet4Mesh::try_from(&PolyMesh3d::from(&mesh).triangulate().unwrap()).unwrap(); + + let model = Tet4Model::from_mesh_and_quadrature(mesh, quadrature.clone()); + let ndof = model.ndof(); + let rho_0 = 3.0; + + let mass_matrix = model.assemble_mass(rho_0).build_dense(); + + // Cells all have the same size + let cell_size = 1.0 / f64::from_usize(*resolution).unwrap(); + let abs_det_J = cell_size * cell_size * cell_size / (6.0 * 4.0); + let num_nodes = model.vertices().len(); + + let mut node_counts = vec![0u32; num_nodes]; + for connectivity in model.connectivity() { + for node_index in &connectivity.0 { + node_counts[*node_index] += 1; + } + } + + let expected_values = node_counts + .iter() + .map(|i| rho_0 * abs_det_J * f64::from(*i)) + .flat_map(|v| once(v).chain(once(v)).chain(once(v))); + let expected = DVector::from_iterator(ndof, expected_values); + + let result = mass_matrix * DVector::repeat(ndof, 1.0); + println!("result: {}", result); + println!("expected: {}", expected); + let diff = result - expected; + assert!(diff.norm() < ndof as f64 * 1e-12); + } +} + +#[test] +fn apply_homogeneous_dirichlet_bc_matrix_simple_example() { + let mut matrix = DMatrix::repeat(8, 8, 2.0); + apply_homogeneous_dirichlet_bc_matrix::(&mut matrix, &[0, 2]); + + #[rustfmt::skip] + let expected = DMatrix::from_column_slice(8, 8, &[ + 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 2.0, 2.0, + 0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 2.0, 2.0, + 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, + 0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 2.0, 2.0, + 0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 2.0, 2.0 + ]); + + let diff = &matrix - &expected; + assert!(diff.norm() < 1e-12); + + // TODO: Design a more elaborate test that also checks for appropriate diagonal scaling + // of the diagonal elements +} + +#[test] +fn apply_homogeneous_dirichlet_bc_csr_simple_example() { + let mut matrix = CsrMatrix::from(&DMatrix::repeat(8, 8, 2.0)); + + apply_homogeneous_dirichlet_bc_csr::<_, U2>(&mut matrix, &[0, 2]); + + // Note: We don't enforce exactly what values the matrix should take on + // the diagonal entries, only that they are somewhere between the + // smallest and largest diagonal entries + #[rustfmt::skip] + let expected = DMatrix::from_column_slice(8, 8, &[ + 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 2.0, 2.0, + 0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 2.0, 2.0, + 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, + 0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 2.0, 2.0, + 0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 2.0, 2.0 + ]); + + // TODO: Assert that the sparsity pattern is as expected + assert_eq!(matrix.build_dense(), expected) + + // TODO: Design a more elaborate test that also checks for appropriate diagonal scaling + // of the diagonal elements +} + +#[test] +fn bilinear_quad_indices_element_variables() { + let u = DVector::from_vec(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0]); + let indices1 = Quad4d2Connectivity([2, 3, 1, 0]); + let indices2 = Quad4d2Connectivity([4, 5, 3, 2]); + let u_quad1: Matrix2x4<_> = indices1.element_variables(&u); + let u_quad2: Matrix2x4<_> = indices2.element_variables(&u); + + assert_eq!( + u_quad1, + Matrix2x4::from_row_slice(&[4.0, 6.0, 2.0, 0.0, 5.0, 7.0, 3.0, 1.0]) + ); + + assert_eq!( + u_quad2, + Matrix2x4::from_row_slice(&[8.0, 10.0, 6.0, 4.0, 9.0, 11.0, 7.0, 5.0]) + ); +} + +/// Creates an instance of a VectorFunction that corresponds to the elastic pseudo forces F(u) +/// given displacements u. +fn create_single_element_elastic_force_vector_function<'a, Connectivity>( + element: &'a Connectivity::Element, + indices: &'a Connectivity, + material: impl ElasticMaterialModel + 'a, + quadrature: impl Quadrature + 'a, +) -> impl VectorFunction + 'a +where + Connectivity: ElementConnectivity>::GeometryDim>, + Connectivity::GeometryDim: + DimNameMul + DimMin, + DefaultAllocator: ElementConnectivityAllocator, +{ + let d = Connectivity::GeometryDim::dim(); + let vector_space_dim = d * Connectivity::NodalDim::dim(); + VectorFunctionBuilder::with_dimension(vector_space_dim).with_function(move |f, u| { + let u_element = indices.element_variables(u); + let elliptic_operator = MaterialEllipticOperator(&material); + let f_element = + assemble_generalized_element_elliptic_term(element, &elliptic_operator, &u_element, &quadrature); + f.copy_from_slice(f_element.as_slice()); + }) +} + +#[test] +fn element_stiffness_matrix_is_negative_derivative_of_forces_for_linear_material_arbitrary_displacement() { + let u = DVector::from_vec(vec![3.0, -2.0, 1.0, -4.0, 13.0, -2.0, 13.0, 15.0]); + let lame = LameParameters { mu: 2.0, lambda: 3.0 }; + let material = LinearElasticMaterial::from(lame); + + let h = 1e-6; + let quadrature = quad_quadrature_strength_5_f64(); + let quad = Quad4d2Element::from_vertices([ + Point2::new(0.5, 0.25), + Point2::new(1.25, 0.5), + Point2::new(1.5, 1.0), + Point2::new(0.25, 1.5), + ]); + + let quad_indices = Quad4d2Connectivity([0, 1, 2, 3]); + + let u_element = quad_indices.element_variables(&u); + let elliptic_operator = MaterialEllipticOperator(&material); + let a = assemble_generalized_element_stiffness(&quad, &elliptic_operator, &u_element, &quadrature); + + let func = create_single_element_elastic_force_vector_function(&quad, &quad_indices, &material, &quadrature); + + let a_approx = -approximate_jacobian(func, &u, &h); + + let diff = a - a_approx; + assert!(diff.norm() < 1e-6); +} + +#[test] +fn tet4_element_stiffness_matrix_is_negative_derivative_of_forces_for_linear_material_arbitrary_displacement() { + let u = DVector::from_vec(vec![0.1, -0.2, 0.1, -0.0, 0.2, -0.1, 0.0, 0.05, 0.1, 0.2, 0.0, -0.2]); + let lame = LameParameters { mu: 2.0, lambda: 3.0 }; + let material = LinearElasticMaterial::from(lame); + + let h = 1e-6; + let quadrature = tet_quadrature_strength_5(); + let tet = Tet4Element::from_vertices([ + Point3::new(-1.0, -0.5, -1.0), + Point3::new(1.0, -0.5, 0.0), + Point3::new(0.0, 1.0, -1.0), + Point3::new(0.0, 0.0, 0.5), + ]); + let tet_conn = Tet4Connectivity([0, 1, 2, 3]); + + let u_element = tet_conn.element_variables(&u); + let elliptic_operator = MaterialEllipticOperator(&material); + let a = assemble_generalized_element_stiffness(&tet, &elliptic_operator, &u_element, &quadrature); + + let func = create_single_element_elastic_force_vector_function(&tet, &tet_conn, &material, &quadrature); + + let a_approx = -approximate_jacobian(func, &u, &h); + + let diff = &a - &a_approx; + assert!(diff.norm() < 1e-6); +} + +#[test] +fn element_stiffness_matrix_is_negative_derivative_of_forces_for_stvk_material_arbitrary_displacement() { + let u = DVector::from_vec(vec![3.0, -2.0, 1.0, -4.0, 13.0, -2.0, 13.0, 15.0]); + let lame = LameParameters { mu: 2.0, lambda: 3.0 }; + let material = StVKMaterial::from(lame); + + let h = 1e-6; + let quadrature = quad_quadrature_strength_5_f64(); + let quad = Quad4d2Element::from_vertices([ + Point2::new(0.5, 0.25), + Point2::new(1.25, 0.5), + Point2::new(1.5, 1.0), + Point2::new(0.25, 1.5), + ]); + + let quad_indices = Quad4d2Connectivity([0, 1, 2, 3]); + + let u_element = quad_indices.element_variables(&u); + let elliptic_operator = MaterialEllipticOperator(&material); + let a = assemble_generalized_element_stiffness(&quad, &elliptic_operator, &u_element, &quadrature); + + let func = create_single_element_elastic_force_vector_function(&quad, &quad_indices, &material, &quadrature); + + let a_approx = -approximate_jacobian(func, &u, &h); + + let diff = a - a_approx; + assert!(diff.norm() < 1e-5); +} + +#[test] +fn element_stiffness_matrix_is_negative_derivative_of_forces_for_stvk_material_problematic_element() { + // This is an example where the eigenvalues of the element matrices turned out to be + // strongly negative + + let u = DVector::from_vec(vec![ + 0.0, + 0.0, + -0.033613342105786675, + -0.21919651627727949, + -0.26977755029543005, + -0.19110852394892108, + 0.0, + 0.0, + ]); + let lame = YoungPoisson { + young: 1e8, + poisson: 0.2, + }; + let material = StVKMaterial::from(lame); + + let h = 1e-6; + let quadrature = quad_quadrature_strength_5_f64(); + let quad = Quad4d2Element::from_vertices([ + Point2::new(0.0, -1.0), + Point2::new(1.0, -1.0), + Point2::new(1.0, 0.0), + Point2::new(0.0, 0.0), + ]); + + let quad_indices = Quad4d2Connectivity([0, 1, 2, 3]); + + let u_element = quad_indices.element_variables(&u); + let elliptic_operator = MaterialEllipticOperator(&material); + let a = assemble_generalized_element_stiffness(&quad, &elliptic_operator, &u_element, &quadrature); + + let func = create_single_element_elastic_force_vector_function(&quad, &quad_indices, &material, &quadrature); + + let a_approx = -approximate_jacobian(func, &u, &h); + + let diff = &a - &a_approx; + + assert!(diff.norm() / (a.norm() + a_approx.norm()) < 1e-5); + + // TODO: Report issue with this matrix to nalgebra as example of failing eigenvalue decomposition +} + +struct MockElementAssembler { + solution_dim: usize, + num_nodes: usize, + element_connectivities: Vec>, +} + +impl ElementConnectivityAssembler for MockElementAssembler { + fn solution_dim(&self) -> usize { + self.solution_dim + } + + fn num_elements(&self) -> usize { + self.element_connectivities.len() + } + + fn num_nodes(&self) -> usize { + self.num_nodes + } + + fn element_node_count(&self, element_index: usize) -> usize { + self.element_connectivities[element_index].len() + } + + fn populate_element_nodes(&self, output: &mut [usize], element_index: usize) { + output.copy_from_slice(&self.element_connectivities[element_index]) + } +} + +#[test] +fn csr_par_assemble_mock_pattern() { + // Solution dim == 1 + + // Empty pattern + { + let element_assembler = MockElementAssembler { + solution_dim: 1, + num_nodes: 0, + element_connectivities: vec![vec![]], + }; + let csr_assembler = CsrParAssembler::::default(); + let pattern = csr_assembler.assemble_pattern(&element_assembler); + let expected_pattern = SparsityPattern::from_offsets_and_indices(0, 0, vec![0], vec![]); + assert_eq!(pattern, expected_pattern); + } + + // Empty pattern + { + let element_assembler = MockElementAssembler { + solution_dim: 2, + num_nodes: 5, + element_connectivities: vec![vec![]], + }; + let csr_assembler = CsrParAssembler::::default(); + let pattern = csr_assembler.assemble_pattern(&element_assembler); + let expected_pattern = SparsityPattern::from_offsets_and_indices(10, 10, vec![0; 11], vec![]); + assert_eq!(pattern, expected_pattern); + } + + // Simple pattern, solution dim == 1 + { + let element_assembler = MockElementAssembler { + solution_dim: 1, + num_nodes: 6, + element_connectivities: vec![vec![0, 1, 2], vec![2, 3], vec![], vec![3, 4, 4, 4, 4, 4, 4]], + }; + let csr_assembler = CsrParAssembler::::default(); + let pattern = csr_assembler.assemble_pattern(&element_assembler); + let expected_pattern = SparsityPattern::from_offsets_and_indices( + 6, + 6, + vec![0, 3, 6, 10, 13, 15, 15], + vec![0, 1, 2, 0, 1, 2, 0, 1, 2, 3, 2, 3, 4, 3, 4], + ); + assert_eq!(pattern, expected_pattern); + } + + // Simple pattern, solution dim == 2 + { + let element_assembler = MockElementAssembler { + solution_dim: 2, + num_nodes: 6, + element_connectivities: vec![vec![0, 1, 2], vec![2, 3], vec![], vec![3, 4, 4, 4, 4, 4, 4]], + }; + let csr_assembler = CsrParAssembler::::default(); + let pattern = csr_assembler.assemble_pattern(&element_assembler); + let expected_pattern = SparsityPattern::from_offsets_and_indices( + 12, + 12, + vec![0, 6, 12, 18, 24, 32, 40, 46, 52, 56, 60, 60, 60], + vec![ + 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 6, 7, 0, 1, + 2, 3, 4, 5, 6, 7, 4, 5, 6, 7, 8, 9, 4, 5, 6, 7, 8, 9, 6, 7, 8, 9, 6, 7, 8, 9, + ], + ); + assert_eq!(pattern, expected_pattern); + } + + // TODO: Would be good to have some property tests... +} diff --git a/fenris/tests/unit_tests/basis.proptest-regressions b/fenris/tests/unit_tests/basis.proptest-regressions new file mode 100644 index 0000000..4c02e09 --- /dev/null +++ b/fenris/tests/unit_tests/basis.proptest-regressions @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 925f952fe3aaca31b20a02512e467f1e4f6b1034d856bf8366a6bdc7e166ad8c # shrinks to mesh = Mesh2d { vertices: [Point { coords: Matrix { data: [0.0, 0.0] } }, Point { coords: Matrix { data: [1.0, 0.0] } }, Point { coords: Matrix { data: [0.0, -1.0] } }, Point { coords: Matrix { data: [1.0, -1.0] } }, Point { coords: Matrix { data: [0.0, -2.0] } }, Point { coords: Matrix { data: [1.0, -2.0] } }], cells: [QuadIndices([2, 3, 1, 0]), QuadIndices([4, 5, 3, 2])] } diff --git a/fenris/tests/unit_tests/basis.rs b/fenris/tests/unit_tests/basis.rs new file mode 100644 index 0000000..2d8a8ea --- /dev/null +++ b/fenris/tests/unit_tests/basis.rs @@ -0,0 +1,137 @@ +use fenris::connectivity::{CellConnectivity, Connectivity}; +use fenris::geometry::proptest_strategies::rectangular_uniform_mesh_strategy; +use fenris::geometry::ConvexPolygon; +use fenris::model::{FiniteElementInterpolator, Quad4Model}; +use fenris::quadrature::quad_quadrature_strength_5_f64; +use fenris::util::flatten_vertically; +use nalgebra::{DVector, Point2, RealField, Vector2, VectorN, U1}; + +use proptest::collection::vec; +use proptest::prelude::*; + +use matrixcompare::assert_scalar_eq; + +use std::convert::TryFrom; + +#[test] +fn interpolate_into() { + let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; + let supported_nodes = vec![ + 0, 1, // Interpolation point 1 + 0, 3, 4, // Interpolation point 2 + 1, 2, 4, // Interpolation point 3 + ]; // Interpolation point 4 + let supported_node_offsets = vec![0, 2, 5, 8, 8]; + + let interpolator = FiniteElementInterpolator::from_compressed_values( + values.into_iter().zip(supported_nodes).collect(), + supported_node_offsets, + ); + + let u = DVector::from_column_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]); + + let mut interpolated_values = vec![Vector2::zeros(); 4]; + + interpolator.interpolate_into(&mut interpolated_values, &u); + + let v = interpolated_values; + assert_scalar_eq!(v[0].x, 7.0); + assert_scalar_eq!(v[0].y, 10.0); + assert_scalar_eq!(v[1].x, 76.0); + assert_scalar_eq!(v[1].y, 88.0); + assert_scalar_eq!(v[2].x, 125.0); + assert_scalar_eq!(v[2].y, 146.0); + assert_scalar_eq!(v[3].x, 0.0); + assert_scalar_eq!(v[3].y, 0.0); +} + +fn find_interior_point(polygon: &ConvexPolygon) -> Point2 +where + T: RealField, +{ + // Find an interior point by averaging all vertices (note: this is not in general the centroid) + let num_vertices = polygon.vertices().len(); + let vertex_sum = polygon + .vertices() + .iter() + .map(|p| p.coords) + .fold(Vector2::zeros(), |a, b| a + b); + Point2::from(vertex_sum / T::from_usize(num_vertices).unwrap()) +} + +proptest! { + #[test] + fn finite_element_interpolator_is_identity_for_node_vertices( + mesh in rectangular_uniform_mesh_strategy(1.0, 8)) + { + prop_assume!(mesh.connectivity().len() > 0); + + let quadrature = quad_quadrature_strength_5_f64(); + let model = Quad4Model::from_mesh_and_quadrature(mesh.clone(), quadrature); + let interpolation_vertices = mesh.vertices(); + let interpolator = model.make_interpolator(interpolation_vertices).unwrap(); + + // Assume that the solution variable is x, with x corresponding to the position + // of vertices. So x_h(X) = sum_i N_i(X) x_i, where x_h(X) is the deformed + // position at reference position X. Then, clearly, if x_i == X_i, with X_i + // being a node in the Lagrange finite element basis, then + // x_h(X_i) == X_i. + let vertices: Vec<_> = mesh.vertices() + .iter() + .map(|x| x.coords) + .collect(); + + let solution_variables = flatten_vertically(&vertices).unwrap(); + let mut result = vec![Vector2::zeros(); interpolation_vertices.len()]; + + interpolator.interpolate_into(&mut result, &solution_variables); + + assert_eq!(result.len(), interpolation_vertices.len()); + for (v_result, v_expected) in result.iter().zip(interpolation_vertices) { + // TODO: Use matrixcompare + let diff = v_result - v_expected.coords; + prop_assert!(diff.norm() <= 1e-6); + } + } + + #[test] + fn finite_element_interpolation_at_interior_points_is_bounded_by_nodal_values( + (mesh, u) in rectangular_uniform_mesh_strategy(1.0, 8).prop_flat_map(|mesh| { + let ndof = mesh.vertices().len(); + let u_strategy = vec(-10.0..10.0, ndof).prop_map(move |v| DVector::from_iterator(ndof, v)); + (Just(mesh), u_strategy) + })) + { + assert_eq!(mesh.vertices().len(), u.len()); + prop_assume!(mesh.connectivity().len() > 0); + + // It is easy to show that |u_h(x)| = |sum_i N_i(x) u_i| <= max_i |u_i|, + // where u_i are the nodal values of the nodes for which x is in the support of the + // nodal basis function. + + let quadrature = quad_quadrature_strength_5_f64(); + let model = Quad4Model::from_mesh_and_quadrature(mesh.clone(), quadrature); + + let interpolation_vertices: Vec<_> = mesh.connectivity().iter() + .map(|connectivity| connectivity.cell(mesh.vertices()).unwrap()) + .map(|cell| ConvexPolygon::try_from(cell).expect("Meshes should only have convex cells")) + .map(|polygon| find_interior_point(&polygon)) + .collect(); + + let interpolator = model.make_interpolator(&interpolation_vertices).unwrap(); + let mut result = vec![VectorN::::zeros(); interpolation_vertices.len()]; + + interpolator.interpolate_into(&mut result, &u); + + prop_assert_eq!(result.len(), interpolation_vertices.len()); + + for (i, u_result) in result.iter().map(|p| p[0]).enumerate() { + let cell_connectivity = mesh.connectivity()[i]; + let smaller_than_neighbor_nodes = cell_connectivity.vertex_indices() + .iter() + .map(|idx| u[*idx]) + .any(|u_j| u_result.abs() <= u_j.abs()); + prop_assert!(smaller_than_neighbor_nodes); + } + } +} diff --git a/fenris/tests/unit_tests/cg.rs b/fenris/tests/unit_tests/cg.rs new file mode 100644 index 0000000..6024a5d --- /dev/null +++ b/fenris/tests/unit_tests/cg.rs @@ -0,0 +1,58 @@ +use crate::assert_approx_matrix_eq; +use fenris::cg::{ConjugateGradient, IdentityOperator, RelativeResidualCriterion}; +use fenris::nalgebra::{DMatrix, DVector}; + +#[test] +fn solve_identity() { + let operator = IdentityOperator; + let mut x = DVector::zeros(4); + let b = DVector::from_element(4, 5.0); + ConjugateGradient::new() + .with_operator(operator) + .with_stopping_criterion(RelativeResidualCriterion::default()) + .solve_with_guess(&b, &mut x) + .unwrap(); + assert_eq!(x, b); +} + +#[test] +fn solve_arbitrary() { + // Use an arbitrary symmetric, positive definite matrix (diagonally dominant in this case) + // and the identity preconditioner + let a = DMatrix::from_fn(3, 3, |r, c| if r == c { 7.0 } else { 2.0 }); + + let x0 = DVector::from_row_slice(&[1.0, 2.0, 3.0]); + let b = &a * &x0; + let mut x = DVector::zeros(3); + let output = ConjugateGradient::new() + .with_operator(&a) + .with_stopping_criterion(RelativeResidualCriterion::new(1e-14)) + .solve_with_guess(&b, &mut x) + .unwrap(); + + assert!(output.num_iterations > 0 && output.num_iterations <= 3); + assert_approx_matrix_eq!(&x, &x0, abstol = 1e-12); +} + +#[test] +fn solve_arbitrary_preconditioned() { + // Take some arbitrary positive definite matrices as system matrix and preconditioner + let a = DMatrix::from_row_slice(3, 3, &[21.0, -1.0, -5.0, -1.0, 11.0, -4.0, -5.0, -4.0, 26.0]); + let p = DMatrix::from_row_slice(3, 3, &[17.0, 6.0, 3.0, 6.0, 14.0, 9.0, 3.0, 9.0, 10.0]); + let x0 = DVector::from_column_slice(&[1.0, 3.0, 2.0]); + let b = &a * &x0; + + // Arbitrary initial guess + let mut x = DVector::from_column_slice(&[2.0, 1.0, 0.0]); + let output = ConjugateGradient::new() + .with_operator(&a) + .with_preconditioner(&p) + .with_stopping_criterion(RelativeResidualCriterion::new(1e-14)) + .solve_with_guess(&b, &mut x) + .unwrap(); + + // We use the fact that CG converges in exact arithmetic in at most n iterations + // for an n x n matrix. For such a small matrix, we should reach high precision immediately. + assert_eq!(output.num_iterations, 3); + assert_approx_matrix_eq!(&x, &x0, abstol = 1e-12); +} diff --git a/fenris/tests/unit_tests/conversion_formulas.rs b/fenris/tests/unit_tests/conversion_formulas.rs new file mode 100644 index 0000000..9c3b0c0 --- /dev/null +++ b/fenris/tests/unit_tests/conversion_formulas.rs @@ -0,0 +1,57 @@ +use fenris::solid::materials::{YoungPoisson, LameParameters}; +use matrixcompare::assert_scalar_eq; + +fn check_effective_parameters(effective_params: YoungPoisson, paper_params: YoungPoisson) { + let LameParameters { mu: mu_eff, .. } = effective_params.into(); + let LameParameters { lambda: lambda_bad, mu } = paper_params.into(); + + let lambda_eff = 2.0 * (mu_eff * effective_params.poisson) / (1.0 - 2.0 * effective_params.poisson); + assert_scalar_eq!(mu_eff, mu, comp = abs, tol = mu_eff * 1e-12); + assert_scalar_eq!(lambda_eff, lambda_bad, comp=abs, tol = lambda_eff * 1e-9); +} + +#[test] +fn errata_conversion_formulas() { + // Test that the formulas for the "effective" material parameters given in the errata are correct. + // We do this by checking that, using these formulas, we obtain the same Lame parameters as our + // incorrect implementation + + // Numerical verification (Section 5.4) + { + let effective_params = YoungPoisson { + young: 2.67857142857e6, + poisson: 0.25 + }; + let paper_params = YoungPoisson { + young: 3e6, + poisson: 0.4 + }; + check_effective_parameters(effective_params, paper_params); + } + + // Twisting cylinder + { + let effective_params = YoungPoisson { + young: 4.82625482625e6, + poisson: 0.42857142857 + }; + let paper_params = YoungPoisson { + young: 5e6, + poisson: 0.48 + }; + check_effective_parameters(effective_params, paper_params); + } + + // Armadillo slingshot + { + let effective_params = YoungPoisson { + young: 4.46428571429e5, + poisson: 0.25 + }; + let paper_params = YoungPoisson { + young: 5e5, + poisson: 0.4 + }; + check_effective_parameters(effective_params, paper_params); + } +} \ No newline at end of file diff --git a/fenris/tests/unit_tests/element.proptest-regressions b/fenris/tests/unit_tests/element.proptest-regressions new file mode 100644 index 0000000..05d5a62 --- /dev/null +++ b/fenris/tests/unit_tests/element.proptest-regressions @@ -0,0 +1,11 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 98a33f1ef95de867c023436d05254517ded8f1dd9aea56a3d0c38fc170be11a7 # shrinks to (x, y) = (0.0, 0.3795635332390195) +cc de24d8bff42796cd3a7e927c1d894874fc96a143cd766409f20a983c3de2acdb # shrinks to tri = Triangle2d([Point { coords: Matrix { data: [0.0, 0.0] } }, Point { coords: Matrix { data: [0.0, -99.65933887502784] } }, Point { coords: Matrix { data: [57.997960082136984, -92.59510743817927] } }]) +cc e3931d1eff663bf05135714b7d16b90b92803e3ca5866e2fd6df92a550efa684 # shrinks to (a, b, xi) = (Point { coords: Matrix { data: [0.0, 0.20939344981708885] } }, Point { coords: Matrix { data: [-37.52723953068697, 6.034261117385057] } }, -0.8591033571234763) +cc 170e251a7fba0d179a7ea0383f4c16ce3acfb0fc96309e0f14ad0bb8d580d8b1 # shrinks to (tet, xi) = (Tet4Element { vertices: [Point { coords: Matrix { data: [0.0, 0.0, -9.82566465994083] } }, Point { coords: Matrix { data: [0.0, -7.794835280149801, 0.0] } }, Point { coords: Matrix { data: [-4.6558498908784385, 0.0, 0.0] } }, Point { coords: Matrix { data: [34.43724993104183, 0.0, 0.0] } }] }, Point { coords: Matrix { data: [0.0, -0.0, -0.0] } }) +cc 999d392d7796f81363dae05aa2816004ba4e764101e31a4886d204d836dd32d8 # shrinks to tri = Triangle([Point { coords: Matrix { data: [-0.5350053492532321, 0.0] } }, Point { coords: Matrix { data: [-82.85181263386065, 90.14126143890536] } }, Point { coords: Matrix { data: [-98.76535656554171, 0.0] } }]) diff --git a/fenris/tests/unit_tests/element.rs b/fenris/tests/unit_tests/element.rs new file mode 100644 index 0000000..4f83614 --- /dev/null +++ b/fenris/tests/unit_tests/element.rs @@ -0,0 +1,865 @@ +use fenris::element::{ + map_physical_coordinates, project_physical_coordinates, FiniteElement, Hex20Element, Hex27Element, Hex8Element, + Quad4d2Element, Quad9d2Element, ReferenceFiniteElement, Segment2d2Element, Tet10Element, Tet20Element, Tet4Element, + Tri3d2Element, Tri6d2Element, +}; +use fenris::error::estimate_element_L2_error; +use fenris::geometry::proptest_strategies::{ + clockwise_triangle2d_strategy_f64, nondegenerate_convex_quad2d_strategy_f64, +}; +use fenris::geometry::{LineSegment2d, Quad2d, Triangle2d}; +use fenris::quadrature::{ + hex_quadrature_strength_11, quad_quadrature_strength_11, tet_quadrature_strength_10, tri_quadrature_strength_5, +}; +use hamilton2::calculus::{approximate_jacobian, VectorFunctionBuilder}; + +use nalgebra::{ + DVectorSlice, Dynamic, MatrixMN, Point2, Point3, RowVector3, RowVector4, RowVector6, RowVectorN, Vector1, Vector2, + Vector3, U1, U10, U2, U20, U27, U3, U4, U6, U8, U9, +}; + +use fenris::util::proptest::point2_f64_strategy; + +use matrixcompare::assert_scalar_eq; + +use proptest::prelude::*; + +use crate::assert_approx_matrix_eq; + +#[test] +fn map_reference_coords_quad2d() { + let vertices = [ + Point2::new(5.0, 3.0), + Point2::new(10.0, 4.0), + Point2::new(11.0, 6.0), + Point2::new(6.0, 4.0), + ]; + let quad = Quad2d(vertices); + let quad = Quad4d2Element::from(quad); + + let x0 = quad.map_reference_coords(&Vector2::new(-1.0, -1.0)); + assert!(x0.relative_eq(&vertices[0].coords, 1e-10, 1e-10)); + + let x1 = quad.map_reference_coords(&Vector2::new(1.0, -1.0)); + assert!(x1.relative_eq(&vertices[1].coords, 1e-10, 1e-10)); + + let x2 = quad.map_reference_coords(&Vector2::new(1.0, 1.0)); + assert!(x2.relative_eq(&vertices[2].coords, 1e-10, 1e-10)); + + let x3 = quad.map_reference_coords(&Vector2::new(-1.0, 1.0)); + assert!(x3.relative_eq(&vertices[3].coords, 1e-10, 1e-10)); +} + +#[test] +fn map_reference_coords_edge2d() { + let a = Point2::new(5.0, 3.0); + let b = Point2::new(10.0, 4.0); + let edge = Segment2d2Element::from(LineSegment2d::new(a, b)); + + let x0 = edge.map_reference_coords(&Vector1::new(-1.0)); + assert!(x0.relative_eq(&a.coords, 1e-10, 1e-10)); + + let x1 = edge.map_reference_coords(&Vector1::new(1.0)); + assert!(x1.relative_eq(&b.coords, 1e-10, 1e-10)); + + let x2 = edge.map_reference_coords(&Vector1::new(0.0)); + assert!(x2.relative_eq(&((a.coords + b.coords) / 2.0), 1e-10, 1e-10)); + + let x3 = edge.map_reference_coords(&Vector1::new(0.5)); + assert!(x3.relative_eq(&(0.25 * a.coords + 0.75 * b.coords), 1e-10, 1e-10)); +} + +#[test] +fn map_physical_coords_quad2d() { + let vertices = [ + Point2::new(5.0, 3.0), + Point2::new(10.0, 4.0), + Point2::new(11.0, 6.0), + Point2::new(6.0, 4.0), + ]; + let quad = Quad2d(vertices); + let quad = Quad4d2Element::from(quad); + + let xi0 = map_physical_coordinates(&quad, &vertices[0]).unwrap(); + assert!(xi0 + .coords + .relative_eq(&Vector2::new(-1.0, -1.0), 1e-10, 1e-10)); + + let xi1 = map_physical_coordinates(&quad, &vertices[1]).unwrap(); + assert!(xi1 + .coords + .relative_eq(&Vector2::new(1.0, -1.0), 1e-10, 1e-10)); + + let xi2 = map_physical_coordinates(&quad, &vertices[2]).unwrap(); + assert!(xi2 + .coords + .relative_eq(&Vector2::new(1.0, 1.0), 1e-10, 1e-10)); + + let xi3 = map_physical_coordinates(&quad, &vertices[3]).unwrap(); + assert!(xi3 + .coords + .relative_eq(&Vector2::new(-1.0, 1.0), 1e-10, 1e-10)); +} + +#[test] +fn tri3d2_lagrange_property() { + // We expect that N_i(x_j) = delta_ij + // where N_i is the ith basis function, j is the vertex associated with the ith node, + // and delta_ij is the Kronecker delta. + let element = Tri3d2Element::reference(); + + for (i, xi) in element.vertices().into_iter().enumerate() { + let phi = element.evaluate_basis(&xi.coords); + + let mut expected = MatrixMN::::zeros(); + expected[i] = 1.0; + + assert_approx_matrix_eq!(phi, expected, abstol = 1e-12); + } +} + +#[test] +fn tri6d2_lagrange_property() { + // We expect that N_i(x_j) = delta_ij + // where N_i is the ith basis function, j is the vertex associated with the ith node, + // and delta_ij is the Kronecker delta. + let element = Tri6d2Element::reference(); + + for (i, xi) in element.vertices().into_iter().enumerate() { + let phi = element.evaluate_basis(&xi.coords); + + let mut expected = MatrixMN::::zeros(); + expected[i] = 1.0; + + assert_approx_matrix_eq!(phi, expected, abstol = 1e-12); + } +} + +#[test] +fn quad9_lagrange_property() { + // We expect that N_i(x_j) = delta_ij + // where N_i is the ith basis function, j is the vertex associated with the ith node, + // and delta_ij is the Kronecker delta. + let element = Quad9d2Element::reference(); + + for (i, xi) in element.vertices().into_iter().enumerate() { + let phi = element.evaluate_basis(&xi.coords); + + let mut expected = MatrixMN::::zeros(); + expected[i] = 1.0; + + assert_approx_matrix_eq!(phi, expected, abstol = 1e-12); + } +} + +#[test] +fn tet4_lagrange_property() { + // We expect that N_i(x_j) = delta_ij + // where N_i is the ith basis function, j is the vertex associated with the ith node, + // and delta_ij is the Kronecker delta. + let element = Tet4Element::reference(); + + for (i, xi) in element.vertices().into_iter().enumerate() { + let phi = element.evaluate_basis(&xi.coords); + + let mut expected = MatrixMN::::zeros(); + expected[i] = 1.0; + + assert_approx_matrix_eq!(phi, expected, abstol = 1e-12); + } +} + +#[test] +fn tet10_lagrange_property() { + // We expect that N_i(x_j) = delta_ij + // where N_i is the ith basis function, j is the vertex associated with the ith node, + // and delta_ij is the Kronecker delta. + let element = Tet10Element::reference(); + + for (i, xi) in element.vertices().into_iter().enumerate() { + let phi = element.evaluate_basis(&xi.coords); + + let mut expected = MatrixMN::::zeros(); + expected[i] = 1.0; + + assert_approx_matrix_eq!(phi, expected, abstol = 1e-12); + } +} + +#[test] +fn tet20_lagrange_property() { + // We expect that N_i(x_j) = delta_ij + // where N_i is the ith basis function, j is the vertex associated with the ith node, + // and delta_ij is the Kronecker delta. + let element = Tet20Element::reference(); + + for (i, xi) in element.vertices().into_iter().enumerate() { + let phi = element.evaluate_basis(&xi.coords); + + let mut expected = MatrixMN::::zeros(); + expected[i] = 1.0; + + assert_approx_matrix_eq!(phi, expected, abstol = 1e-12); + } +} + +#[test] +fn hex8_lagrange_property() { + // We expect that N_i(x_j) = delta_ij + // where N_i is the ith basis function, j is the vertex associated with the ith node, + // and delta_ij is the Kronecker delta. + let element = Hex8Element::reference(); + + for (i, xi) in element.vertices().into_iter().enumerate() { + let phi = element.evaluate_basis(&xi.coords); + + let mut expected = MatrixMN::::zeros(); + expected[i] = 1.0; + + assert_approx_matrix_eq!(phi, expected, abstol = 1e-12); + } +} + +#[test] +fn hex27_lagrange_property() { + // We expect that N_i(x_j) = delta_ij + // where N_i is the ith basis function, j is the vertex associated with the ith node, + // and delta_ij is the Kronecker delta. + let element = Hex27Element::reference(); + + for (i, xi) in element.vertices().into_iter().enumerate() { + let phi = element.evaluate_basis(&xi.coords); + + let mut expected = MatrixMN::::zeros(); + expected[i] = 1.0; + + assert_approx_matrix_eq!(phi, expected, abstol = 1e-12); + } +} + +#[test] +fn hex20_lagrange_property() { + // We expect that N_i(x_j) = delta_ij + // where N_i is the ith basis function, j is the vertex associated with the ith node, + // and delta_ij is the Kronecker delta. + let element = Hex20Element::reference(); + + for (i, xi) in element.vertices().into_iter().enumerate() { + let phi = element.evaluate_basis(&xi.coords); + + let mut expected = MatrixMN::::zeros(); + expected[i] = 1.0; + + assert_approx_matrix_eq!(phi, expected, abstol = 1e-12); + } +} + +#[test] +fn quad4_bilinear_function_exact_error() { + let quad = Quad2d([ + Point2::new(-1.0, -1.0), + Point2::new(2.0, -2.0), + Point2::new(4.0, 1.0), + Point2::new(-2.0, 3.0), + ]); + let element = Quad4d2Element::from(quad); + // If u_exact is a bilinear function, then we can exactly represent it + // with a Quad4 element. Then the basis weights of u_h are given by + // u_exact(x_i), where x_i are the coordinates of each node of the element. + let u_exact = |p: &Point2| { + let x = p[0]; + let y = p[1]; + Vector1::new(5.0 * x * y + 3.0 * x - 2.0 * y - 5.0) + }; + let u_weights = RowVector4::from_columns(&quad.0.iter().map(|x| u_exact(x)).collect::>()); + + // TODO: Use lower strength quadrature + let quadrature = quad_quadrature_strength_11(); + let error = estimate_element_L2_error(&element, |p, _| u_exact(p), &u_weights, &quadrature); + + // Note: The solution here is obtained by symbolic integration. See + // the accompanying notebooks + assert_scalar_eq!( + error, + (9955.0f64 / 12.0).sqrt(), + comp = abs, + tol = element.diameter() * 1e-12 + ); +} + +#[test] +fn hex27_triquadratic_function_exact_error() { + let element = Hex27Element::reference(); + // If u_exact is a triquadratic function, then we can exactly represent it + // with a Hex27 element. Then the basis weights of u_h are given by + // u_exact(x_i), where x_i are the coordinates of each node of the element. + let u_exact = |p: &Point3| { + let x = p[0]; + let y = p[1]; + let z = p[2]; + // Three arbitrary quadratic functions in each argument x, y, z + let f = 3.0 * x * x - 2.0 * x + 5.0; + let g = -2.0 * y * y + 3.0 * y + 1.5; + let h = 1.5 * z * z + 1.2 * z - 3.0; + + Vector1::new(f * g * h) + }; + let cols: Vec<_> = element.vertices().iter().map(|x| u_exact(x)).collect(); + let u_weights = RowVectorN::<_, U27>::from_columns(&cols); + + // TODO: Use lower strength quadrature + let quadrature = hex_quadrature_strength_11(); + let error = estimate_element_L2_error(&element, |p, _| u_exact(p), &u_weights, &quadrature); + + // Note: The solution here is obtained by symbolic integration. See + // the accompanying notebooks + assert_scalar_eq!(error, 0.0, comp = abs, tol = 1e-12); +} + +#[test] +fn hex20_quadratic_function_exact_error() { + let element = Hex20Element::reference(); + // If u_exact is a quadratic function, then we can exactly represent it + // with a Hex20 element. Then the basis weights of u_h are given by + // u_exact(x_i), where x_i are the coordinates of each node of the element. + // Note that the space of functions spanned by the Hex20 basis functions is larger + // than the space of quadratic functions, so this is not a sufficient condition + // for checking correctness + let u_exact = |p: &Point3| { + let x = p[0]; + let y = p[1]; + let z = p[2]; + + Vector1::new( + 2.0 * x * x + 4.0 * y * y - 3.0 * z * z + 3.0 * x * y - 5.0 * x * z + 1.5 * y * z + 3.0 * x - 2.0 * y + + 3.0 * z + + 9.0, + ) + }; + let cols: Vec<_> = element.vertices().iter().map(|x| u_exact(x)).collect(); + let u_weights = RowVectorN::<_, U20>::from_columns(&cols); + + // TODO: Use lower strength quadrature + let quadrature = hex_quadrature_strength_11(); + let error = estimate_element_L2_error(&element, |p, _| u_exact(p), &u_weights, &quadrature); + + // Note: The solution here is obtained by symbolic integration. See + // the accompanying notebooks + assert_scalar_eq!(error, 0.0, comp = abs, tol = 1e-12); +} + +// TODO: Test all gradients of basis functions + +fn point_in_tri_ref_domain() -> impl Strategy> { + // Generate points x, y in [-1, 1]^2 such that + // x + y <= 0 + (-1.0..=1.0) + .prop_flat_map(|x: f64| (Just(x), -1.0..=-x)) + .prop_map(|(x, y)| Point2::new(x, y)) +} + +fn point_in_quad_ref_domain() -> impl Strategy> { + // Generate points x, y, z in [-1, 1]^3 + let r = -1.0..=1.0; + [r.clone(), r].prop_map(|[x, y]| Point2::new(x, y)) +} + +fn point_in_hex_ref_domain() -> impl Strategy> { + // Generate points x, y, z in [-1, 1]^3 + let r = -1.0..=1.0; + [r.clone(), r.clone(), r].prop_map(|[x, y, z]| Point3::new(x, y, z)) +} + +fn point_in_tet_ref_domain() -> impl Strategy> { + // Generate points x, y, z in [-1, 1]^3 such that + // x + y + z <= 0 + (-1.0..=1.0) + .prop_flat_map(|x: f64| (Just(x), -1.0..=-x)) + .prop_flat_map(|(x, y)| { + let z_range = -1.0..=(-(x + y)); + (Just(x), Just(y), z_range) + }) + .prop_map(|(x, y, z)| Point3::new(x, y, z)) +} + +macro_rules! partition_of_unity_test { + ($test_name:ident, $ref_domain_strategy:expr, $ref_element:expr) => { + proptest! { + #[test] + fn $test_name(xi in $ref_domain_strategy) { + let xi = xi.coords; + let element = $ref_element; + let phi = element.evaluate_basis(&xi); + let phi_sum: f64 = phi.sum(); + + prop_assert!( (phi_sum - 1.0f64).abs() <= 1e-12); + } + } + }; +} + +macro_rules! partition_of_unity_gradient_test { + ($test_name:ident, $ref_domain_strategy:expr, $ref_element:expr) => { + proptest! { + #[test] + fn $test_name(xi in $ref_domain_strategy) { + // Since the sum of basis functions is 1, the sum of the gradients must be 0 + let xi = xi.coords; + let element = $ref_element; + let grad = element.gradients(&xi); + let grad_sum = grad.column_sum(); + + let mut zero = grad_sum.clone(); + zero.fill(0.0); + + assert_approx_matrix_eq!(grad_sum, zero, abstol=1e-12); + } + } + }; +} + +partition_of_unity_test!( + tri3d2_partition_of_unity, + point_in_tri_ref_domain(), + Tri3d2Element::reference() +); +partition_of_unity_test!( + tri6d2_partition_of_unity, + point_in_tri_ref_domain(), + Tri6d2Element::reference() +); +partition_of_unity_test!( + quad4_partition_of_unity, + point_in_quad_ref_domain(), + Tri6d2Element::reference() +); +partition_of_unity_test!( + quad9_partition_of_unity, + point_in_quad_ref_domain(), + Tri6d2Element::reference() +); + +partition_of_unity_test!( + hex27_partition_of_unity, + point_in_hex_ref_domain(), + Hex27Element::reference() +); + +partition_of_unity_test!( + hex20_partition_of_unity, + point_in_hex_ref_domain(), + Hex20Element::reference() +); + +partition_of_unity_gradient_test!( + tri3d2_partition_of_unity_gradient, + point_in_tri_ref_domain(), + Tri3d2Element::reference() +); +partition_of_unity_gradient_test!( + tri6d2_partition_of_unity_gradient, + point_in_tri_ref_domain(), + Tri6d2Element::reference() +); +partition_of_unity_gradient_test!( + quad4_partition_of_unity_gradient, + point_in_quad_ref_domain(), + Quad4d2Element::reference() +); +partition_of_unity_gradient_test!( + quad9_partition_of_unity_gradient, + point_in_quad_ref_domain(), + Quad9d2Element::reference() +); + +partition_of_unity_gradient_test!( + hex27_partition_of_unity_gradient, + point_in_hex_ref_domain(), + Hex27Element::reference() +); + +partition_of_unity_gradient_test!( + hex20_partition_of_unity_gradient, + point_in_hex_ref_domain(), + Hex20Element::reference() +); + +proptest! { + #[test] + fn tri3_affine_function_error_is_zero(tri in clockwise_triangle2d_strategy_f64()) { + let element = Tri3d2Element::from(tri); + // If u_exact is an affine function, then we can exactly represent it + // with a Tri3 element. Then the basis weights of u_h are given by + // u_exact(x_i), where x_i are the coordinates of each node of the element. + let u_exact = |x: &Point2| Vector1::new(2.0 * x[0] - 3.0 * x[1] + 1.5); + let u_weights = RowVector3::from_columns(&tri.0.iter().map(|x| u_exact(x)).collect::>()); + + // TODO: Use lower strength quadrature + let quadrature = tri_quadrature_strength_5(); + let error = estimate_element_L2_error(&element, |p, _| u_exact(p), &u_weights, quadrature); + + assert_scalar_eq!(error, 0.0, comp=abs, tol=element.diameter() * 1e-12); + } + + #[test] + fn tri6_affine_function_error_is_zero(tri in clockwise_triangle2d_strategy_f64()) { + let element = Tri6d2Element::from(Tri3d2Element::from(tri)); + // If u_exact is an affine function, then we can exactly represent it + // with a Tri3 element. Then the basis weights of u_h are given by + // u_exact(x_i), where x_i are the coordinates of each node of the element. + let u_exact = |x: &Point2| Vector1::new(2.0 * x[0] - 3.0 * x[1] + 1.5); + let u_weights = RowVector6::from_columns(&element.vertices().iter().map(|x| u_exact(x)).collect::>()); + + // TODO: Use lower strength quadrature + let quadrature = tri_quadrature_strength_5(); + let error = estimate_element_L2_error(&element, |p, _| u_exact(p), &u_weights, quadrature); + + assert_scalar_eq!(error, 0.0, comp=abs, tol=element.diameter() * 1e-12); + } + + #[test] + fn tri6_quadratic_function_error_is_zero(tri in clockwise_triangle2d_strategy_f64()) { + let element = Tri6d2Element::from(Tri3d2Element::from(tri)); + // If u_exact is an affine function, then we can exactly represent it + // with a Tri3 element. Then the basis weights of u_h are given by + // u_exact(x_i), where x_i are the coordinates of each node of the element. + let u_exact = |x: &Point2| Vector1::new(2.0 * x[0] * x[0] - 3.0 * x[1] * x[0] + 0.5 * x[1] + 1.5); + let u_weights = RowVector6::from_columns(&element.vertices().iter().map(|x| u_exact(x)).collect::>()); + + // TODO: Use lower strength quadrature + let quadrature = tri_quadrature_strength_5(); + let error = estimate_element_L2_error(&element, |p, _| u_exact(p), &u_weights, quadrature); + + // TODO: Check tolerance + assert_scalar_eq!(error, 0.0, comp=abs, tol=element.diameter() * element.diameter() * 1e-12); + } + + #[test] + fn quad4_affine_function_error_is_zero(quad in nondegenerate_convex_quad2d_strategy_f64()) { + let element = Quad4d2Element::from(quad); + // If u_exact is an affine function, then we can exactly represent it + // with a Quad4 element. Then the basis weights of u_h are given by + // u_exact(x_i), where x_i are the coordinates of each node of the element. + // Note that this is not true for a general bilinear function unless + // the quad and the reference quad are related by an affine transformation + // (i.e. the quad is a parallellogram). + let u_exact = |x: &Point2| { + let y = x[1]; + let x = x[0]; + Vector1::new(3.0 * x + 2.0 * y - 3.0) + }; + let u_weights = RowVector4::from_columns(&quad.0 + .iter() + .map(|x| u_exact(x)) + .collect::>()); + + let quadrature = quad_quadrature_strength_11(); + let error = estimate_element_L2_error(&element, |p, _| u_exact(p), &u_weights, &quadrature); + + assert_scalar_eq!(error, 0.0, comp=abs, tol=element.diameter() * 1e-12); + } + + #[test] + fn edge2d_element_jacobian_is_derivative_of_transform( + (a, b, xi) in (point2_f64_strategy(), point2_f64_strategy(), -1.0..=1.0) + ) { + let segment = LineSegment2d::new(a, b); + let element = Segment2d2Element::from(segment); + let xi = Vector1::new(xi); + + // Finite difference parameter + let h = 1e-6; + let hvec = Vector1::new(h); + + // TODO: Extend VectorFunction and approximate_jacobian to allow + // maps between domains of different dimension + let j = element.reference_jacobian(&xi); + + // Approximate Jacobian by finite differences + let x_plus = element.map_reference_coords(&(xi + hvec)); + let x_minus = element.map_reference_coords(&(xi - hvec)); + let j_approx = (x_plus - x_minus) / (2.0 * h); + + let tol = (a.coords + b.coords).norm() * h; + assert_approx_matrix_eq!(j, j_approx, abstol=tol); + + } + + #[test] + fn edge2d_element_project_physical_coords_with_perturbation( + (a, b, xi, eps) in (point2_f64_strategy(), point2_f64_strategy(), + -1.0..=1.0, prop_oneof!(Just(0.0), -1.0 .. 1.0)) + ) { + let segment = LineSegment2d::new(a, b); + + prop_assume!(segment.length() > 0.0); + + let element = Segment2d2Element::from(segment); + let xi = Vector1::new(xi); + let x = element.map_reference_coords(&xi); + // Perturb the surface point in the direction normal to the surface. This checks + // that the projection actually still manages to correctly reproduce the original point. + let x_perturbed = &x + eps * segment.normal_dir(); + let xi_proj = project_physical_coordinates(&element, &Point2::from(x_perturbed)).unwrap(); + let x_reconstructed = element.map_reference_coords(&xi_proj.coords); + + prop_assert!((x_reconstructed - x).norm() <= 1e-12); + } + + #[test] + fn tet4_partition_of_unity(xi in point_in_tet_ref_domain()) { + let element = Tet4Element::reference(); + let phi = element.evaluate_basis(&xi.coords); + let phi_sum: f64 = phi.sum(); + + dbg!(phi_sum); + prop_assert!( (phi_sum - 1.0f64).abs() <= 1e-12); + } + + #[test] + fn tet10_partition_of_unity(xi in point_in_tet_ref_domain()) { + let element = Tet10Element::reference(); + let phi = element.evaluate_basis(&xi.coords); + let phi_sum: f64 = phi.sum(); + + dbg!(phi_sum); + prop_assert!( (phi_sum - 1.0f64).abs() <= 1e-12); + } + + #[test] + fn tet20_partition_of_unity(xi in point_in_tet_ref_domain()) { + let element = Tet20Element::reference(); + let phi = element.evaluate_basis(&xi.coords); + let phi_sum: f64 = phi.sum(); + + dbg!(phi_sum); + prop_assert!( (phi_sum - 1.0f64).abs() <= 1e-12); + } + + #[test] + fn hex8_partition_of_unity(xi in point_in_hex_ref_domain()) { + let element = Hex8Element::reference(); + let phi = element.evaluate_basis(&xi.coords); + let phi_sum: f64 = phi.sum(); + + dbg!(phi_sum); + prop_assert!( (phi_sum - 1.0f64).abs() <= 1e-12); + } + + #[test] + fn tet4_partition_of_unity_gradient((x, y, z) in (-1.0 ..= 1.0, -1.0 ..= 1.0, -1.0 ..= 1.0)) { + // Since the sum of basis functions is 1, the sum of the gradients must be 0 + let xi = Vector3::new(x, y, z); + let element = Tet4Element::reference(); + let grad = element.gradients(&xi); + let grad_sum = grad.column_sum(); + + assert_approx_matrix_eq!(grad_sum, Vector3::::zeros(), abstol=1e-12); + } + + #[test] + fn tet10_partition_of_unity_gradient((x, y, z) in (-1.0 ..= 1.0, -1.0 ..= 1.0, -1.0 ..= 1.0)) { + // Since the sum of basis functions is 1, the sum of the gradients must be 0 + let xi = Vector3::new(x, y, z); + let element = Tet10Element::reference(); + let grad = element.gradients(&xi); + let grad_sum = grad.column_sum(); + + assert_approx_matrix_eq!(grad_sum, Vector3::::zeros(), abstol=1e-12); + } + + #[test] + fn tet20_partition_of_unity_gradient((x, y, z) in (-1.0 ..= 1.0, -1.0 ..= 1.0, -1.0 ..= 1.0)) { + // Since the sum of basis functions is 1, the sum of the gradients must be 0 + let xi = Vector3::new(x, y, z); + let element = Tet20Element::reference(); + let grad = element.gradients(&xi); + let grad_sum = grad.column_sum(); + + assert_approx_matrix_eq!(grad_sum, Vector3::::zeros(), abstol=1e-12); + } + + #[test] + fn tet4_affine_function_error_is_zero(element in any::>()) { + // If u_exact is an affine function, then we can exactly represent it + // with a Tet4 element. Then the basis weights of u_h are given by + // u_exact(x_i), where x_i are the coordinates of each node of the element. + let u_exact = |p: &Point3| { + let x = p.x; + let y = p.y; + let z = p.z; + Vector1::new(3.0 * x + 2.0 * y - 3.0 * z + 3.0) + }; + let u_weights = RowVector4::from_columns(&element.vertices() + .iter() + .map(|x| u_exact(x)) + .collect::>()); + + let quadrature = tet_quadrature_strength_10(); + let error = estimate_element_L2_error(&element, |p, _| u_exact(p), &u_weights, &quadrature); + + assert_scalar_eq!(error, 0.0, comp=abs, tol=element.diameter() * 1e-12); + } + + #[test] + fn tri3d2_element_gradient_is_derivative_of_transform( + (tri, xi) in (any::>(), point_in_tri_ref_domain()) + ) { + + let elem = Tri3d2Element::from(tri); + + // Finite difference parameter + let h = 1e-6; + // Note: Function values are given as row vectors, so we transpose to get the result, + // and we must also transpose the end result + let f = VectorFunctionBuilder::with_dimension(3).with_function(move |x, xi| { + x.copy_from(&elem.evaluate_basis(&xi.fixed_slice::(0, 0).clone_owned()).transpose()); + }); + + let grad = elem.gradients(&xi.coords); + let grad_approx = approximate_jacobian(f, &DVectorSlice::<_, Dynamic>::from(&xi.coords).clone_owned(), &h).transpose(); + + assert_approx_matrix_eq!(grad, &grad_approx, abstol=1e-5); + } + + #[test] + fn tri6d2_element_gradient_is_derivative_of_transform( + (tri, xi) in (any::>(), point_in_tri_ref_domain()) + ) { + + let elem = Tri6d2Element::from(Tri3d2Element::from(tri)); + + // Finite difference parameter + let h = 1e-6; + // Note: Function values are given as row vectors, so we transpose to get the result, + // and we must also transpose the end result + let f = VectorFunctionBuilder::with_dimension(6).with_function(move |x, xi| { + x.copy_from(&elem.evaluate_basis(&xi.fixed_slice::(0, 0).clone_owned()).transpose()); + }); + + let grad = elem.gradients(&xi.coords); + let grad_approx = approximate_jacobian(f, &DVectorSlice::<_, Dynamic>::from(&xi.coords).clone_owned(), &h).transpose(); + + assert_approx_matrix_eq!(grad, &grad_approx, abstol=1e-5); + } + + #[test] + fn tet4_element_gradient_is_derivative_of_transform( + (tet, xi) in (any::>(), point_in_tet_ref_domain()) + ) { + // Finite difference parameter + let h = 1e-6; + // Note: Function values are given as row vectors, so we transpose to get the result, + // and we must also transpose the end result + let f = VectorFunctionBuilder::with_dimension(4).with_function(move |x, xi| { + x.copy_from(&tet.evaluate_basis(&xi.fixed_slice::(0, 0).clone_owned()).transpose()); + }); + + let grad = tet.gradients(&xi.coords); + let grad_approx = approximate_jacobian(f, &DVectorSlice::<_, Dynamic>::from(&xi.coords).clone_owned(), &h).transpose(); + + assert_approx_matrix_eq!(grad, &grad_approx, abstol=1e-5); + } + + #[test] + fn tet10_reference_element_gradient_is_derivative_of_transform( + xi in point_in_tet_ref_domain() + ) { + let tet = Tet10Element::reference(); + // Finite difference parameter + let h = 1e-6; + // Note: Function values are given as row vectors, so we transpose to get the result, + // and we must also transpose the end result + let f = VectorFunctionBuilder::with_dimension(10).with_function(move |x, xi| { + x.copy_from(&tet.evaluate_basis(&xi.fixed_slice::(0, 0).clone_owned()).transpose()); + }); + + let grad = tet.gradients(&xi.coords); + let grad_approx = approximate_jacobian(f, &DVectorSlice::<_, Dynamic>::from(&xi.coords).clone_owned(), &h).transpose(); + + assert_approx_matrix_eq!(grad, &grad_approx, abstol=1e-5); + } + + #[test] + fn tet20_reference_element_gradient_is_derivative_of_transform( + xi in point_in_tet_ref_domain() + ) { + let tet = Tet20Element::reference(); + // Finite difference parameter + let h = 1e-6; + // Note: Function values are given as row vectors, so we transpose to get the result, + // and we must also transpose the end result + let f = VectorFunctionBuilder::with_dimension(20).with_function(move |x, xi| { + x.copy_from(&tet.evaluate_basis(&xi.fixed_slice::(0, 0).clone_owned()).transpose()); + }); + + let grad = tet.gradients(&xi.coords); + let grad_approx = approximate_jacobian(f, &DVectorSlice::<_, Dynamic>::from(&xi.coords).clone_owned(), &h).transpose(); + + assert_approx_matrix_eq!(grad, &grad_approx, abstol=1e-5); + } + + #[test] + fn hex27_reference_element_gradient_is_derivative_of_transform( + xi in point_in_hex_ref_domain() + ) { + let hex = Hex27Element::reference(); + // Finite difference parameter + let h = 1e-6; + // Note: Function values are given as row vectors, so we transpose to get the result, + // and we must also transpose the end result + let f = VectorFunctionBuilder::with_dimension(27).with_function(move |x, xi| { + x.copy_from(&hex.evaluate_basis(&xi.fixed_slice::(0, 0).clone_owned()).transpose()); + }); + + let grad = hex.gradients(&xi.coords); + let xi = DVectorSlice::<_, Dynamic>::from(&xi.coords).clone_owned(); + let grad_approx = approximate_jacobian(f, &xi, &h).transpose(); + + assert_approx_matrix_eq!(grad, &grad_approx, abstol=1e-5); + } + + #[test] + fn hex20_reference_element_gradient_is_derivative_of_transform( + xi in point_in_hex_ref_domain() + ) { + let hex = Hex20Element::reference(); + // Finite difference parameter + let h = 1e-6; + // Note: Function values are given as row vectors, so we transpose to get the result, + // and we must also transpose the end result + let f = VectorFunctionBuilder::with_dimension(20).with_function(move |x, xi| { + x.copy_from(&hex.evaluate_basis(&xi.fixed_slice::(0, 0).clone_owned()).transpose()); + }); + + let grad = hex.gradients(&xi.coords); + let xi = DVectorSlice::<_, Dynamic>::from(&xi.coords).clone_owned(); + let grad_approx = approximate_jacobian(f, &xi, &h).transpose(); + + assert_approx_matrix_eq!(grad, &grad_approx, abstol=1e-5); + } + + #[test] + fn tet4_element_jacobian_is_derivative_of_transform( + (tet, xi) in (any::>(), point_in_tet_ref_domain()) + ) { + // Finite difference parameter + let h = 1e-6; + // Function is x = f(xi) + let f = VectorFunctionBuilder::with_dimension(3).with_function(move |x, xi| { + x.copy_from(&tet.map_reference_coords(&xi.fixed_slice::(0, 0).clone_owned())); + }); + + let j = tet.reference_jacobian(&xi.coords); + let j_approx = approximate_jacobian(f, &DVectorSlice::<_, Dynamic>::from(&xi.coords).clone_owned(), &h); + assert_approx_matrix_eq!(j, &j_approx, abstol=1e-5); + } + + #[test] + fn tet4_element_jacobian_has_non_negative_jacobian( + (tet, xi) in (any::>(), point_in_tet_ref_domain()) + ) { + let j = tet.reference_jacobian(&xi.coords); + prop_assert!(dbg!(j.determinant()) >= 0.0); + } +} diff --git a/fenris/tests/unit_tests/embedding.rs b/fenris/tests/unit_tests/embedding.rs new file mode 100644 index 0000000..cc6e7e2 --- /dev/null +++ b/fenris/tests/unit_tests/embedding.rs @@ -0,0 +1,152 @@ +use fenris::mesh::TriangleMesh2d; + +use fenris::element::{FiniteElement, Hex8Element, Quad4d2Element}; +use fenris::embedding::{ + compute_element_embedded_quadrature, construct_embedded_quadrature, embed_mesh_2d, QuadratureOptions, +}; +use fenris::geometry::procedural::{create_rectangular_uniform_hex_mesh, create_rectangular_uniform_quad_mesh_2d}; +use fenris::quadrature::{ + hex_quadrature_strength_11, tet_quadrature_strength_10, tri_quadrature_strength_5_f64, Quadrature, +}; +use nalgebra::{Point2, Point3, Vector2, Vector3}; + +use fenris::connectivity::{CellConnectivity, Tri3d2Connectivity}; +use fenris::geometry::polymesh::PolyMesh3d; +use matrixcompare::assert_scalar_eq; + +#[test] +fn embedded_trimesh_integrates_to_correct_area() { + let a = Point2::new(1.0, 1.0); + let b = Point2::new(2.0, 3.0); + let c = Point2::new(3.0, 0.0); + let d = Point2::new(2.0, -2.0); + let e = Point2::new(5.0, -1.0); + + let trimesh = { + let vertices = vec![a, b, c, d, e]; + let cells = vec![ + Tri3d2Connectivity([0, 2, 1]), + Tri3d2Connectivity([3, 2, 0]), + Tri3d2Connectivity([3, 4, 2]), + ]; + TriangleMesh2d::from_vertices_and_connectivity(vertices, cells) + }; + + let top_left = Vector2::new(0.0, 4.0); + let background_mesh = create_rectangular_uniform_quad_mesh_2d(3.0, 2, 2, 1, &top_left); + + let embedded = embed_mesh_2d(&background_mesh, &trimesh).unwrap(); + let interface_element_embedding = embedded.clone().into_iter().map(|(cell_index, polygons)| { + let cell = background_mesh.connectivity()[cell_index] + .cell(background_mesh.vertices()) + .unwrap(); + let quad_element = Quad4d2Element::from(cell); + (quad_element, polygons) + }); + + // TODO: Remove this scope after debugging is complete + // { + // use fenris::geometry::vtk::{create_vtk_data_set_from_polygons, write_vtk}; + // use vtkio::model::DataSet; + // let embedded_data_set = DataSet::from(&trimesh); + // write_vtk( + // embedded_data_set, + // "data/test_trimesh.vtk", + // "test trimesh", + // ).unwrap(); + // + // let background_data_set = DataSet::from(&background_mesh); + // write_vtk( + // background_data_set, + // "data/test_background_mesh.vtk", + // "test background mesh", + // ).unwrap(); + // + // let polygons = embedded.clone() + // .into_iter() + // .map(|(_, polygons)| polygons) + // .flatten() + // .collect::>(); + // let embedded_data_set = create_vtk_data_set_from_polygons(&polygons); + // write_vtk( + // embedded_data_set, + // "data/test_embedded_polygons.vtk", + // " test embedded polygons", + // ).unwrap(); + // } + + let quadratures = + construct_embedded_quadrature(interface_element_embedding.clone(), tri_quadrature_strength_5_f64()); + assert_eq!(quadratures.len(), embedded.len()); + + let area: f64 = interface_element_embedding + .zip(quadratures) + .map(|((quad_element, _), quadrature)| { + quadrature.integrate(|p| quad_element.reference_jacobian(p).determinant()) + }) + .sum(); + + assert_scalar_eq!(area, 7.5, comp = abs, tol = 1e-12); +} + +#[test] +fn compute_element_embedded_quadrature_test() { + // Resolution of embedded mesh + let resolutions = [1, 2]; + + for res in &resolutions { + // We consider a Hexahedron shape [0, 0.5]^3 represented by several smaller hexahedra, + // embedded in a single larger Hexahedron [-1, 1]^3 element. + + let hex_quadrature = hex_quadrature_strength_11::(); + // Since the mapping from the reference element to our hexahedral element is linear, + // and the mapping from the reference tet to each individual tet is linear, + // we should be able to integrate polynomials of at least degree 8 (10 - 2) given a + // 10-strength quadrature rule for our tets + // TODO: Maybe use a bg element that is not the reference element for better generality + let tet_quadrature = tet_quadrature_strength_10::(); + let bg_element = Hex8Element::::reference(); + + // We use an element with the exact shape of the intersection to use as a + // "ground truth" solution + let ground_truth_element = Hex8Element::from_vertices([ + Point3::new(0.0, 0.0, 0.0), + Point3::new(0.5, 0.0, 0.0), + Point3::new(0.5, 0.5, 0.0), + Point3::new(0.0, 0.5, 0.0), + Point3::new(0.0, 0.0, 0.5), + Point3::new(0.5, 0.0, 0.5), + Point3::new(0.5, 0.5, 0.5), + Point3::new(0.0, 0.5, 0.5), + ]); + + let f_separate_coords = + |x: f64, y: f64, z: f64| x.powi(5) * z.powi(3) + x.powi(3) * y.powi(3) * z + x + y + z + 5.0; + let f = |x: &Vector3| f_separate_coords(x.x, x.y, x.z); + + let embedded_intersection = PolyMesh3d::from(&create_rectangular_uniform_hex_mesh(0.5, 1, 1, 1, *res)); + + let (weights, points) = compute_element_embedded_quadrature( + &bg_element, + &embedded_intersection, + tet_quadrature, + &QuadratureOptions::default(), + ) + .unwrap(); + + // Since our element is identical to the reference element, the sum of the quadrature + // weights must be equal to the volume. + assert_scalar_eq!(weights.iter().sum::(), 0.125, comp = abs, tol = 1e-12); + + let quadrature = (weights, points); + let integral: f64 = quadrature.integrate(f); + + let expected_integral: f64 = hex_quadrature.integrate(|xi| { + let x = ground_truth_element.map_reference_coords(xi); + let j = ground_truth_element.reference_jacobian(xi); + j.determinant().abs() * f(&x) + }); + + assert_scalar_eq!(integral, expected_integral, comp = abs, tol = 1e-12); + } +} diff --git a/fenris/tests/unit_tests/fe_mesh.rs b/fenris/tests/unit_tests/fe_mesh.rs new file mode 100644 index 0000000..70634eb --- /dev/null +++ b/fenris/tests/unit_tests/fe_mesh.rs @@ -0,0 +1,129 @@ +use fenris::mesh::{Hex27Mesh, Mesh2d, Mesh3d}; +use nalgebra::{Point2, Point3, Vector2, Vector3}; + +use crate::assert_approx_matrix_eq; +use fenris::connectivity::{ + Hex8Connectivity, Quad4d2Connectivity, Quad9d2Connectivity, Tri3d2Connectivity, Tri6d2Connectivity, +}; + +#[test] +fn quad4_to_quad9_single_element_mesh() { + let vertices = vec![ + Point2::new(2.0, 3.0), + Point2::new(3.0, 3.0), + Point2::new(3.0, 4.0), + Point2::new(2.0, 4.0), + ]; + let quad4 = Quad4d2Connectivity([0, 1, 2, 3]); + let quad4_mesh = Mesh2d::from_vertices_and_connectivity(vertices.clone(), vec![quad4]); + + let quad9_mesh = Mesh2d::::from(quad4_mesh); + let quad9_connectivity = quad9_mesh.connectivity().first().unwrap(); + + assert_eq!(quad9_connectivity[0..4], [0, 1, 2, 3]); + assert_eq!(quad9_connectivity[4..8], [4, 5, 6, 7]); + assert_eq!(quad9_connectivity[8], 8); + + const EPS: f64 = 1e-12; + assert_approx_matrix_eq!(quad9_mesh.vertices()[0].coords, vertices[0].coords, abstol = EPS); + assert_approx_matrix_eq!(quad9_mesh.vertices()[1].coords, vertices[1].coords, abstol = EPS); + assert_approx_matrix_eq!(quad9_mesh.vertices()[2].coords, vertices[2].coords, abstol = EPS); + assert_approx_matrix_eq!(quad9_mesh.vertices()[3].coords, vertices[3].coords, abstol = EPS); + assert_approx_matrix_eq!(quad9_mesh.vertices()[4].coords, Vector2::new(2.5, 3.0), abstol = EPS); + assert_approx_matrix_eq!(quad9_mesh.vertices()[5].coords, Vector2::new(3.0, 3.5), abstol = EPS); + assert_approx_matrix_eq!(quad9_mesh.vertices()[6].coords, Vector2::new(2.5, 4.0), abstol = EPS); + assert_approx_matrix_eq!(quad9_mesh.vertices()[7].coords, Vector2::new(2.0, 3.5), abstol = EPS); + assert_approx_matrix_eq!(quad9_mesh.vertices()[8].coords, Vector2::new(2.5, 3.5), abstol = EPS); +} + +#[test] +fn tri3_to_tri6_single_element_mesh() { + let vertices = vec![Point2::new(2.0, 3.0), Point2::new(3.0, 3.0), Point2::new(3.0, 4.0)]; + let tri3 = Tri3d2Connectivity([0, 1, 2]); + let tri3_mesh = Mesh2d::from_vertices_and_connectivity(vertices.clone(), vec![tri3]); + + let tri6_mesh = Mesh2d::::from(tri3_mesh); + let tri6_connectivity = tri6_mesh.connectivity().first().unwrap(); + + assert_eq!(tri6_connectivity[0..3], [0, 1, 2]); + assert_eq!(tri6_connectivity[3..6], [3, 4, 5]); + + const EPS: f64 = 1e-12; + assert_approx_matrix_eq!(tri6_mesh.vertices()[0].coords, vertices[0].coords, abstol = EPS); + assert_approx_matrix_eq!(tri6_mesh.vertices()[1].coords, vertices[1].coords, abstol = EPS); + assert_approx_matrix_eq!(tri6_mesh.vertices()[2].coords, vertices[2].coords, abstol = EPS); + assert_approx_matrix_eq!(tri6_mesh.vertices()[3].coords, Vector2::new(2.5, 3.0), abstol = EPS); + assert_approx_matrix_eq!(tri6_mesh.vertices()[4].coords, Vector2::new(3.0, 3.5), abstol = EPS); + assert_approx_matrix_eq!(tri6_mesh.vertices()[5].coords, Vector2::new(2.5, 3.5), abstol = EPS); +} + +#[test] +fn hex8_to_hex27_single_element_mesh() { + let vertices = vec![ + Point3::new(2.0, 3.0, 1.0), + Point3::new(4.0, 3.0, 1.0), + Point3::new(4.0, 4.0, 1.0), + Point3::new(2.0, 4.0, 1.0), + Point3::new(2.0, 3.0, 5.0), + Point3::new(4.0, 3.0, 5.0), + Point3::new(4.0, 4.0, 5.0), + Point3::new(2.0, 4.0, 5.0), + ]; + let hex8 = Hex8Connectivity([0, 1, 2, 3, 4, 5, 6, 7]); + let hex8_mesh = Mesh3d::from_vertices_and_connectivity(vertices.clone(), vec![hex8]); + + let hex27_mesh = Hex27Mesh::from(&hex8_mesh); + let hex27_connectivity = hex27_mesh.connectivity().first().unwrap(); + + assert_eq!(hex27_connectivity.0[0..8], [0, 1, 2, 3, 4, 5, 6, 7]); + // assert_eq!(tri6_connectivity[3..6], [3, 4, 5]); + + const EPS: f64 = 1e-12; + + let v = hex27_mesh.vertices(); + + // Vertex nodes + assert_approx_matrix_eq!(v[0].coords, vertices[0].coords, abstol = EPS); + assert_approx_matrix_eq!(v[1].coords, vertices[1].coords, abstol = EPS); + assert_approx_matrix_eq!(v[2].coords, vertices[2].coords, abstol = EPS); + assert_approx_matrix_eq!(v[3].coords, vertices[3].coords, abstol = EPS); + assert_approx_matrix_eq!(v[4].coords, vertices[4].coords, abstol = EPS); + assert_approx_matrix_eq!(v[5].coords, vertices[5].coords, abstol = EPS); + assert_approx_matrix_eq!(v[6].coords, vertices[6].coords, abstol = EPS); + assert_approx_matrix_eq!(v[7].coords, vertices[7].coords, abstol = EPS); + + let edge_midpoint = |a: usize, b: usize| (vertices[a].coords + vertices[b].coords) / 2.0; + let midpoint = |indices: &[usize]| { + indices + .iter() + .copied() + .map(|i| vertices[i]) + .fold(Vector3::zeros(), |acc, v| acc + v.coords) + / (indices.len() as f64) + }; + + // Edge nodes + assert_approx_matrix_eq!(v[8].coords, edge_midpoint(0, 1), abstol = EPS); + assert_approx_matrix_eq!(v[9].coords, edge_midpoint(0, 3), abstol = EPS); + assert_approx_matrix_eq!(v[10].coords, edge_midpoint(0, 4), abstol = EPS); + assert_approx_matrix_eq!(v[11].coords, edge_midpoint(1, 2), abstol = EPS); + assert_approx_matrix_eq!(v[12].coords, edge_midpoint(1, 5), abstol = EPS); + assert_approx_matrix_eq!(v[13].coords, edge_midpoint(2, 3), abstol = EPS); + assert_approx_matrix_eq!(v[14].coords, edge_midpoint(2, 6), abstol = EPS); + assert_approx_matrix_eq!(v[15].coords, edge_midpoint(3, 7), abstol = EPS); + assert_approx_matrix_eq!(v[16].coords, edge_midpoint(4, 5), abstol = EPS); + assert_approx_matrix_eq!(v[17].coords, edge_midpoint(4, 7), abstol = EPS); + assert_approx_matrix_eq!(v[18].coords, edge_midpoint(5, 6), abstol = EPS); + assert_approx_matrix_eq!(v[19].coords, edge_midpoint(6, 7), abstol = EPS); + + // Face nodes + assert_approx_matrix_eq!(v[20].coords, midpoint(&[0, 1, 2, 3]), abstol = EPS); + assert_approx_matrix_eq!(v[21].coords, midpoint(&[0, 1, 5, 4]), abstol = EPS); + assert_approx_matrix_eq!(v[22].coords, midpoint(&[0, 3, 7, 4]), abstol = EPS); + assert_approx_matrix_eq!(v[23].coords, midpoint(&[1, 2, 6, 5]), abstol = EPS); + assert_approx_matrix_eq!(v[24].coords, midpoint(&[2, 3, 6, 7]), abstol = EPS); + assert_approx_matrix_eq!(v[25].coords, midpoint(&[4, 5, 6, 7]), abstol = EPS); + + // Center node + assert_approx_matrix_eq!(v[26].coords, midpoint(&[0, 1, 2, 3, 4, 5, 6, 7]), abstol = EPS); +} diff --git a/fenris/tests/unit_tests/geometry.rs b/fenris/tests/unit_tests/geometry.rs new file mode 100644 index 0000000..af0d4d0 --- /dev/null +++ b/fenris/tests/unit_tests/geometry.rs @@ -0,0 +1,329 @@ +use crate::assert_approx_matrix_eq; +use fenris::geometry::{ConvexPolyhedron, Distance, Hexahedron, SignedDistance, Tetrahedron, Triangle}; +use matrixcompare::assert_scalar_eq; +use nalgebra::{Point2, Point3, Vector2}; + +#[test] +fn triangle_signed_distance_and_distance() { + let triangle = Triangle([Point2::new(1.0, 2.0), Point2::new(4.0, 0.0), Point2::new(3.0, 3.0)]); + + // Outside, closest to edge 0 + { + let p = Point2::new(1.0, 0.0); + let sdf = triangle.query_signed_distance(&p).unwrap(); + assert_eq!(sdf.feature_id, 0); + assert_approx_matrix_eq!( + sdf.point.coords, + Vector2::new(1.9230769230769, 1.3846153846154), + abstol = 1e-10 + ); + assert_scalar_eq!(sdf.signed_distance, 1.6641005886757, comp = abs, tol = 1e-10); + assert_scalar_eq!(triangle.distance(&p), 1.6641005886757, comp = abs, tol = 1e-10); + } + + // Outside, closest to vertex 1 + { + let p = Point2::new(5.0, 0.0); + let sdf = triangle.query_signed_distance(&p).unwrap(); + // Closest edge is ambiguous, can be either 0 or 1 + assert!([0, 1].contains(&sdf.feature_id)); + assert_approx_matrix_eq!(sdf.point.coords, Vector2::new(4.0, 0.0), abstol = 1e-10); + assert_scalar_eq!(sdf.signed_distance, 1.0, comp = abs, tol = 1e-10); + assert_scalar_eq!(triangle.distance(&p), 1.0, comp = abs, tol = 1e-10); + } + + // Outside, closest to edge 1 + { + let p = Point2::new(4.0, 3.0); + let sdf = triangle.query_signed_distance(&p).unwrap(); + // Closest edge is ambiguous, can be either 0 or 1 + assert_eq!(sdf.feature_id, 1); + assert_approx_matrix_eq!(sdf.point.coords, Vector2::new(3.1, 2.7), abstol = 1e-10); + assert_scalar_eq!(sdf.signed_distance, 0.9486832980505, comp = abs, tol = 1e-10); + assert_scalar_eq!(triangle.distance(&p), 0.9486832980505, comp = abs, tol = 1e-10); + } + + // Outside, closest to edge 2 + { + let p = Point2::new(2.0, 3.0); + let sdf = triangle.query_signed_distance(&p).unwrap(); + // Closest edge is ambiguous, can be either 0 or 1 + assert_eq!(sdf.feature_id, 2); + assert_approx_matrix_eq!(sdf.point.coords, Vector2::new(2.2, 2.6), abstol = 1e-10); + assert_scalar_eq!(sdf.signed_distance, 0.4472135955, comp = abs, tol = 1e-10); + assert_scalar_eq!(triangle.distance(&p), 0.4472135955, comp = abs, tol = 1e-10); + } + + // Inside, closest to edge 0 + { + let p = Point2::new(3.0, 1.0); + let sdf = triangle.query_signed_distance(&p).unwrap(); + // Closest edge is ambiguous, can be either 0 or 1 + assert_eq!(sdf.feature_id, 0); + assert_approx_matrix_eq!( + sdf.point.coords, + Vector2::new(2.8461538461538, 0.7692307692308), + abstol = 1e-10 + ); + assert_scalar_eq!(sdf.signed_distance, -0.2773500981126, comp = abs, tol = 1e-10); + assert_eq!(triangle.distance(&p), 0.0); + } + + // Inside, closest to edge 1 + { + let p = Point2::new(3.0, 2.0); + let sdf = triangle.query_signed_distance(&p).unwrap(); + // Closest edge is ambiguous, can be either 0 or 1 + assert_eq!(sdf.feature_id, 1); + assert_approx_matrix_eq!(sdf.point.coords, Vector2::new(3.3, 2.1), abstol = 1e-10); + assert_scalar_eq!(sdf.signed_distance, -0.3162277660168, comp = abs, tol = 1e-10); + assert_eq!(triangle.distance(&p), 0.0); + } + + // Inside, closest to edge 2 + { + let p = Point2::new(2.0, 2.0); + let sdf = triangle.query_signed_distance(&p).unwrap(); + // Closest edge is ambiguous, can be either 0 or 1 + assert_eq!(sdf.feature_id, 2); + assert_approx_matrix_eq!(sdf.point.coords, Vector2::new(1.8, 2.4), abstol = 1e-10); + assert_scalar_eq!(sdf.signed_distance, -0.4472135955, comp = abs, tol = 1e-10); + assert_eq!(triangle.distance(&p), 0.0); + } +} + +#[test] +fn cube_polyhedron_signed_distance() { + let cube = Hexahedron::from_vertices([ + Point3::new(-1.0, -1.0, -1.0), + Point3::new(1.0, -1.0, -1.0), + Point3::new(1.0, 1.0, -1.0), + Point3::new(-1.0, 1.0, -1.0), + Point3::new(-1.0, -1.0, 1.0), + Point3::new(1.0, -1.0, 1.0), + Point3::new(1.0, 1.0, 1.0), + Point3::new(-1.0, 1.0, 1.0), + ]); + + // First test points on the outside, one for each face + { + // Face 0 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(-0.5, -0.5, -1.6)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(-0.5, -0.5, -1.0), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, 0.6, comp = abs, tol = 1e-12); + assert_eq!(sdf_result.feature_id, 0); + } + + // Face 1 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(-0.5, -1.3, 0.5)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(-0.5, -1.0, 0.5), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, 0.3, comp = abs, tol = 1e-12); + assert_eq!(sdf_result.feature_id, 1); + } + + // Face 2 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(1.5, 0.5, -0.5)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(1.0, 0.5, -0.5), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, 0.5, comp = abs, tol = 1e-12); + assert_eq!(sdf_result.feature_id, 2); + } + + // Face 3 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(-0.5, 1.4, 0.5)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(-0.5, 1.0, 0.5), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, 0.4, comp = abs, tol = 1e-12); + assert_eq!(sdf_result.feature_id, 3); + } + + // Face 4 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(-1.5, -0.5, -0.5)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(-1.0, -0.5, -0.5), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, 0.5, comp = abs, tol = 1e-12); + assert_eq!(sdf_result.feature_id, 4); + } + + // Face 5 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(0.5, -0.5, 1.2)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(0.5, -0.5, 1.0), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, 0.2, comp = abs, tol = 1e-12); + assert_eq!(sdf_result.feature_id, 5); + } + } + + // Then test points outside, whose closest point is a vertex + // (i.e. in the Voronoi regions of the vertices) + { + // Note: The face given as "feature_id" is somewhat arbitrary, but it must be + // one of the faces that contain the vertex + + // Vertex 0 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(-2.0, -2.0, -2.0)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(-1.0, -1.0, -1.0), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, f64::sqrt(3.0), comp = abs, tol = 1e-12); + assert!([0, 1, 4].contains(&sdf_result.feature_id)); + } + + // Vertex 1 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(2.0, -2.0, -2.0)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(1.0, -1.0, -1.0), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, f64::sqrt(3.0), comp = abs, tol = 1e-12); + assert!([0, 1, 2].contains(&sdf_result.feature_id)); + } + + // Vertex 2 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(2.0, 2.0, -2.0)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(1.0, 1.0, -1.0), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, f64::sqrt(3.0), comp = abs, tol = 1e-12); + assert!([0, 2, 3].contains(&sdf_result.feature_id)); + } + + // Vertex 3 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(-2.0, 2.0, -2.0)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(-1.0, 1.0, -1.0), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, f64::sqrt(3.0), comp = abs, tol = 1e-12); + assert!([0, 3, 4].contains(&sdf_result.feature_id)); + } + + // Vertex 4 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(-2.0, -2.0, 2.0)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(-1.0, -1.0, 1.0), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, f64::sqrt(3.0), comp = abs, tol = 1e-12); + assert!([1, 4, 5].contains(&sdf_result.feature_id)); + } + + // Vertex 5 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(2.0, -2.0, 2.0)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(1.0, -1.0, 1.0), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, f64::sqrt(3.0), comp = abs, tol = 1e-12); + assert!([1, 2, 5].contains(&sdf_result.feature_id)); + } + + // Vertex 6 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(2.0, 2.0, 2.0)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(1.0, 1.0, 1.0), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, f64::sqrt(3.0), comp = abs, tol = 1e-12); + assert!([2, 3, 5].contains(&sdf_result.feature_id)); + } + + // Vertex 7 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(-2.0, 2.0, 2.0)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(-1.0, 1.0, 1.0), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, f64::sqrt(3.0), comp = abs, tol = 1e-12); + assert!([3, 4, 5].contains(&sdf_result.feature_id)); + } + } + + // Test faces on the inside + { + // Face 0 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(-0.5, -0.5, -0.9)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(-0.5, -0.5, -1.0), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, -0.1, comp = abs, tol = 1e-12); + assert_eq!(sdf_result.feature_id, 0); + } + + // Face 1 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(-0.5, -0.8, 0.5)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(-0.5, -1.0, 0.5), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, -0.2, comp = abs, tol = 1e-12); + assert_eq!(sdf_result.feature_id, 1); + } + + // Face 2 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(0.8, 0.5, -0.5)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(1.0, 0.5, -0.5), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, -0.2, comp = abs, tol = 1e-12); + assert_eq!(sdf_result.feature_id, 2); + } + + // Face 3 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(-0.5, 0.9, 0.5)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(-0.5, 1.0, 0.5), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, -0.1, comp = abs, tol = 1e-12); + assert_eq!(sdf_result.feature_id, 3); + } + + // Face 4 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(-0.8, -0.5, -0.5)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(-1.0, -0.5, -0.5), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, -0.2, comp = abs, tol = 1e-12); + assert_eq!(sdf_result.feature_id, 4); + } + + // Face 5 + { + let sdf_result = cube + .query_signed_distance(&Point3::new(0.5, -0.5, 0.7)) + .unwrap(); + assert_approx_matrix_eq!(sdf_result.point, Point3::new(0.5, -0.5, 1.0), abstol = 1e-12); + assert_scalar_eq!(sdf_result.signed_distance, -0.3, comp = abs, tol = 1e-12); + assert_eq!(sdf_result.feature_id, 5); + } + } +} + +#[test] +fn convex_polygon_3d_compute_volume() { + let tetrahedron = Tetrahedron::::reference(); + assert_scalar_eq!(tetrahedron.compute_volume(), 4.0 / 3.0, comp = abs, tol = 1e-12); + + let hexahedron = Hexahedron::::reference(); + assert_scalar_eq!(hexahedron.compute_volume(), 8.0, comp = abs, tol = 1e-12); +} diff --git a/fenris/tests/unit_tests/materials.rs b/fenris/tests/unit_tests/materials.rs new file mode 100644 index 0000000..7aa130e --- /dev/null +++ b/fenris/tests/unit_tests/materials.rs @@ -0,0 +1,267 @@ +use fenris::solid::materials::*; +use fenris::solid::ElasticMaterialModel; +use nalgebra::{ + DMatrix, DMatrixSliceMut, DefaultAllocator, DimName, Dynamic, Matrix2, Matrix3, MatrixMN, MatrixN, MatrixSliceMN, + Vector2, Vector3, U1, U2, U3, +}; + +use crate::assert_approx_matrix_eq; +use nalgebra::allocator::Allocator; +use paste; + +/// Assert that material contractions are consistent with finite difference results +#[allow(non_snake_case)] +fn assert_material_contraction_consistent_with_finite_difference( + material_instances: &[impl ElasticMaterialModel], + deformation_gradients: &[MatrixN], + a: &MatrixMN, +) where + D: DimName, + DefaultAllocator: Allocator + Allocator + Allocator, +{ + // Finite difference step parameter + let h = 1e-6; + let num_nodes = a.ncols(); + for material in material_instances { + for F in deformation_gradients { + for i in 0..num_nodes { + for j in 0..num_nodes { + let a_i = a.fixed_slice::(0, i).clone_owned(); + let a_j = a.fixed_slice::(0, j).clone_owned(); + + let finite_diff = approximate_stiffness_contraction(material, F, &a_i, &a_j, h); + let contraction = material.contract_stress_tensor_with(&F, &a_i, &a_j); + + let scale = f64::max(finite_diff.amax(), contraction.amax()); + let abstol = scale * h; + + assert_approx_matrix_eq!(&finite_diff, &contraction, abstol = abstol); + } + } + } + } +} + +/// Assert that material contractions are consistent between single and batch contractions. +#[allow(non_snake_case)] +fn assert_material_consistent_contractions( + material_instances: &[impl ElasticMaterialModel], + deformation_gradients: &[MatrixN], + a: &MatrixMN, +) where + D: DimName, + DefaultAllocator: Allocator + Allocator, +{ + use std::ops::AddAssign; + + let dim = D::dim(); + let num_nodes = a.ncols(); + + // Arbitrary value to initialize matrices with. We use this to test that the batch + // contraction does not overwrite existing elements in the matrix, but rather + // adds its contributions. + let fill_value = 4.0; + + for material in material_instances { + for F in deformation_gradients { + let output_dim = num_nodes * dim; + let mut output = DMatrix::repeat(output_dim, output_dim, fill_value); + + for i in 0..num_nodes { + for j in 0..num_nodes { + let a_i = a.fixed_slice::(0, i).clone_owned(); + let a_j = a.fixed_slice::(0, j).clone_owned(); + let block_ij = material.contract_stress_tensor_with(F, &a_i, &a_j); + output + .index_mut((i * dim..(i + 1) * dim, j * dim..(j + 1) * dim)) + .add_assign(&block_ij); + } + } + + let mut batch_output = DMatrix::repeat(output_dim, output_dim, fill_value); + material.contract_multiple_stress_tensors_into( + &mut DMatrixSliceMut::from(&mut batch_output), + F, + &MatrixSliceMN::from(a), + ); + + let scale = f64::max(output.amax(), batch_output.amax()); + let abstol = 1e-12 * scale; + + assert_approx_matrix_eq!(&output, &batch_output, abstol = abstol); + } + } +} + +macro_rules! test_material_derivatives { + ($material_name:ident, + $materials:expr, + $deformation_gradients:expr, + $contraction_vectors:expr + $(, postfix = $postfix:ident)?) => { + // Use paste to automatically generate method names for the tests, + // and include a postfix for all test names + paste::item! { + /// Assert that the material is consistent between contract and contract_multiple + #[test] + #[allow(non_snake_case)] + pub fn [<$material_name _contraction_batch_consistency $($postfix )?>]() { + use std::borrow::Borrow; + assert_material_consistent_contractions( + $materials.as_ref(), + $deformation_gradients.as_ref(), + $contraction_vectors.borrow() + ); + } + + /// Assert that the material is approximately consistent with a finite difference + /// discretization of the derivative + #[test] + #[allow(non_snake_case)] + pub fn [<$material_name _contraction_consistent_with_finite_difference $($postfix )?>]() { + use std::borrow::Borrow; + assert_material_contraction_consistent_with_finite_difference( + $materials.as_ref(), + $deformation_gradients.as_ref(), + $contraction_vectors.borrow() + ); + } + } + } +} + +macro_rules! test_material_derivatives_2d { + ($material_name:ident, $materials:expr) => { + test_material_derivatives!( + $material_name, + $materials, + test_deformation_gradients_2d(), + contraction_test_vectors_2d(), + postfix = _2d + ); + }; +} + +macro_rules! test_material_derivatives_3d { + ($material_name:ident, $materials:expr) => { + test_material_derivatives!( + $material_name, + $materials, + test_deformation_gradients_3d(), + contraction_test_vectors_3d(), + postfix = _3d + ); + }; +} + +#[allow(non_snake_case)] +fn test_deformation_gradients_2d() -> Vec> { + vec![ + // Identity corresponds to zero deformation + Matrix2::identity(), + // Singular values [2, 0.4] (non-inverted, mild anisotropic deformation) + Matrix2::new(-1.65561115, 0.85243405, -0.83017955, -0.05576592), + // Singular values [1e2, 1e-2] (non-inverted, strong anisotropic deformation) + Matrix2::new(-11.11160623, -44.72115803, 21.42155763, 86.12587997), + ] +} + +#[allow(non_snake_case)] +fn test_deformation_gradients_3d() -> Vec> { + vec![ + // Identity corresponds to zero deformation + Matrix3::identity(), + // Singular values [2.0, 0.7, 0.5] (non-inverted, mild anisotropic deformation) + Matrix3::new( + 0.28316466, + 1.08445104, + -1.38765817, + 0.03281728, + -1.01281521, + 0.3086884, + -0.54924397, + -0.28600612, + -0.22925947, + ), + // Singular values [1e2, 2, 1e-2] (non-inverted, strong anisotropic deformation) + Matrix3::new( + 52.85734952, + -19.73633697, + -30.87845429, + 26.67331831, + -10.35380109, + -16.15435165, + 58.20810674, + -20.01345825, + -31.60294891, + ), + ] +} + +fn contraction_test_vectors_2d() -> MatrixMN { + MatrixMN::<_, U2, Dynamic>::from_columns(&[Vector2::new(2.0, 3.0), Vector2::new(-1.0, 2.0), Vector2::new(4.0, 1.0)]) +} + +fn contraction_test_vectors_3d() -> MatrixMN { + MatrixMN::<_, U3, Dynamic>::from_columns(&[ + Vector3::new(1.0, -2.0, 3.0), + Vector3::new(-3.0, 0.5, 1.0), + Vector3::new(2.0, -1.0, -0.5), + ]) +} + +fn young_poisson_test_parameters() -> Vec> { + vec![ + YoungPoisson { + young: 1e2, + poisson: 0.1, + }, + YoungPoisson { + young: 1e6, + poisson: 0.1, + }, + YoungPoisson { + young: 1e2, + poisson: 0.45, + }, + YoungPoisson { + young: 1e6, + poisson: 0.45, + }, + ] +} + +fn lame_test_parameters() -> Vec> { + young_poisson_test_parameters() + .into_iter() + .map(LameParameters::from) + .collect() +} + +fn stable_neo_hookean_test_materials() -> Vec> { + lame_test_parameters() + .into_iter() + .map(StableNeoHookeanMaterial::from) + .collect() +} + +fn linear_elastic_test_materials() -> Vec> { + lame_test_parameters() + .into_iter() + .map(LinearElasticMaterial::from) + .collect() +} + +fn stvk_test_materials() -> Vec> { + lame_test_parameters() + .into_iter() + .map(StVKMaterial::from) + .collect() +} + +test_material_derivatives_2d!(stable_neo_hookean, stable_neo_hookean_test_materials()); +test_material_derivatives_3d!(stable_neo_hookean, stable_neo_hookean_test_materials()); +test_material_derivatives_2d!(linear_elastic, linear_elastic_test_materials()); +test_material_derivatives_3d!(linear_elastic, linear_elastic_test_materials()); +test_material_derivatives_2d!(stvk, stvk_test_materials()); +test_material_derivatives_3d!(stvk, stvk_test_materials()); diff --git a/fenris/tests/unit_tests/mesh.proptest-regressions b/fenris/tests/unit_tests/mesh.proptest-regressions new file mode 100644 index 0000000..27c0419 --- /dev/null +++ b/fenris/tests/unit_tests/mesh.proptest-regressions @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 30fceec597d68f4be3af06a9661f6d26c791c150a5818382f8eb24fac730766f # shrinks to (mesh, cell_indices) = (Mesh2d { vertices: [], cells: [] }, [0]) diff --git a/fenris/tests/unit_tests/mesh.rs b/fenris/tests/unit_tests/mesh.rs new file mode 100644 index 0000000..6fb7566 --- /dev/null +++ b/fenris/tests/unit_tests/mesh.rs @@ -0,0 +1,297 @@ +use fenris::geometry::proptest_strategies::rectangular_uniform_mesh_strategy; + +use proptest::collection::vec; +use proptest::prelude::*; + +use fenris::connectivity::{CellConnectivity, Connectivity, Quad9d2Connectivity, Tri3d2Connectivity}; +use fenris::geometry::{Orientation, Triangle}; +use fenris::mesh::{Mesh, Mesh2d}; +use std::cmp::max; + +use fenris::geometry::polymesh::PolyMesh; +use fenris::geometry::procedural::{ + create_rectangular_uniform_hex_mesh, create_rectangular_uniform_quad_mesh_2d, + create_unit_square_uniform_quad_mesh_2d, +}; +use itertools::{equal, sorted, Itertools}; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, DimName, Point2, Scalar, Vector2}; + +#[test] +fn quad4_find_boundary_faces() { + // Single quad + { + let mesh = create_unit_square_uniform_quad_mesh_2d::(1); + let boundary_faces = mesh.find_boundary_faces(); + + let cells: Vec<_> = boundary_faces + .iter() + .cloned() + .map(|(_, cell, _)| cell) + .collect(); + let mut local_indices: Vec<_> = boundary_faces + .iter() + .cloned() + .map(|(_, _, idx)| idx) + .collect(); + local_indices.sort(); + + assert_eq!(cells, [0, 0, 0, 0]); + assert_eq!(local_indices, [0, 1, 2, 3]); + } +} + +#[test] +fn quad9_find_boundary_vertices() { + { + // Single element + let mesh: Mesh2d = create_unit_square_uniform_quad_mesh_2d(1).into(); + let boundary_vertex_indices = mesh.find_boundary_vertices(); + + assert_eq!(boundary_vertex_indices, vec![0, 1, 2, 3, 4, 5, 6, 7]); + } + + { + // Two elements + let vertices = vec![ + Point2::new(0.0, 0.0), // 0 + Point2::new(1.0, 0.0), // 1 + Point2::new(2.0, 0.0), // 2 + Point2::new(0.0, 1.0), // 3 + Point2::new(1.0, 1.0), // 4 + Point2::new(0.0, 1.0), // 5 + Point2::new(0.5, 0.0), // 6 + Point2::new(1.5, 0.0), // 7 + Point2::new(2.0, 0.5), // 8 + Point2::new(1.5, 0.5), // 9 + Point2::new(1.0, 0.5), // 10 + Point2::new(0.5, 0.5), // 11 + Point2::new(0.0, 0.5), // 12 + Point2::new(0.5, 1.0), // 13 + Point2::new(1.5, 1.0), // 14 + ]; + let connectivity = vec![ + Quad9d2Connectivity([0, 1, 4, 5, 6, 10, 13, 12, 11]), + Quad9d2Connectivity([1, 2, 3, 4, 7, 8, 14, 10, 9]), + ]; + let mesh = Mesh2d::from_vertices_and_connectivity(vertices, connectivity); + let boundary_vertex_indices = mesh.find_boundary_vertices(); + + assert_eq!(boundary_vertex_indices, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 13, 14]); + } +} + +#[test] +fn winding_order() { + let a = Point2::new(2.0, 1.0); + let b = Point2::new(3.0, 2.0); + let c = Point2::new(0.0, 3.0); + + use Orientation::{Clockwise, Counterclockwise}; + + assert_eq!(Triangle([a, b, c]).orientation(), Clockwise); + assert_eq!(Triangle([b, c, a]).orientation(), Clockwise); + assert_eq!(Triangle([c, a, b]).orientation(), Clockwise); + assert_eq!(Triangle([a, c, b]).orientation(), Counterclockwise); + assert_eq!(Triangle([b, a, c]).orientation(), Counterclockwise); + assert_eq!(Triangle([c, b, a]).orientation(), Counterclockwise); + + let mut triangle = Triangle([a, b, c]); + triangle.swap_vertices(0, 1); + assert_eq!(triangle.orientation(), Counterclockwise); + triangle.swap_vertices(2, 1); + assert_eq!(triangle.orientation(), Clockwise); + triangle.swap_vertices(2, 0); + assert_eq!(triangle.orientation(), Counterclockwise); + + // No-op swaps don't change winding + triangle.swap_vertices(0, 0); + assert_eq!(triangle.orientation(), Counterclockwise); + triangle.swap_vertices(1, 1); + assert_eq!(triangle.orientation(), Counterclockwise); + triangle.swap_vertices(2, 2); + assert_eq!(triangle.orientation(), Counterclockwise); +} + +fn verify_connected_poly_mesh(original_mesh: &Mesh, poly_mesh: &PolyMesh) +where + T: Scalar, + D: DimName, + C: Connectivity, + DefaultAllocator: Allocator, +{ + assert_eq!(original_mesh.vertices(), poly_mesh.vertices()); + assert_eq!(original_mesh.connectivity().len(), poly_mesh.num_cells()); + + let face_cell_connectivity = poly_mesh.compute_face_cell_connectivity(); + assert!( + face_cell_connectivity + .iter() + .all(|cells| [1, 2].contains(&cells.len())), + "Every face must be connected to either 1 or 2 cells." + ); + + let mut poly_faces_referenced = vec![false; poly_mesh.num_faces()]; + + // Verify that every cell face is contained exactly once in face connectivities of the polymesh + for cell in original_mesh.connectivity() { + let num_cell_faces = cell.num_faces(); + for i in 0..num_cell_faces { + let face = cell.get_face_connectivity(i).unwrap(); + + let topologically_equivalent_poly_faces: Vec<_> = poly_mesh + .face_connectivity_iter() + .enumerate() + .filter(|(_, vertex_indices)| { + equal(sorted(vertex_indices.to_vec()), sorted(face.vertex_indices().to_vec())) + }) + .map(|(i, _)| i) + .collect(); + + assert_eq!( + topologically_equivalent_poly_faces.len(), + 1, + "Every cell face must be present exactly once in the polymesh face connectivity" + ); + + let referenced_face_idx = topologically_equivalent_poly_faces.first().unwrap(); + poly_faces_referenced[*referenced_face_idx] = true; + } + } + + assert!( + poly_faces_referenced.iter().all(|referenced| *referenced), + "All faces in poly mesh must be equivalent to at least one local cell face \ + in the original mesh." + ); +} + +#[test] +fn convert_mesh_to_poly_mesh() { + { + // Single element 2D triangle mesh + + // The exact coordinates should not matter as the conversion is an entirely + // topological conversion + let vertices = vec![Point2::::origin(); 3]; + let connectivity = vec![Tri3d2Connectivity([0, 1, 2])]; + let mesh = Mesh::from_vertices_and_connectivity(vertices.clone(), connectivity); + let polymesh = PolyMesh::from(&mesh); + + assert_eq!(polymesh.vertices(), vertices.as_slice()); + assert_eq!(polymesh.num_cells(), 1); + assert_eq!(polymesh.num_faces(), 3); + + let cell0_sorted_indices = polymesh + .get_cell_connectivity(0) + .unwrap() + .iter() + .cloned() + .sorted(); + assert_eq!(cell0_sorted_indices.as_slice(), &[0, 1, 2]); + + for face_conn in &[[0, 1], [1, 2], [2, 0]] { + let count_equal = polymesh + .face_connectivity_iter() + .filter(|poly_face_conn| poly_face_conn == face_conn) + .count(); + assert_eq!(count_equal, 1); + } + + verify_connected_poly_mesh(&mesh, &polymesh); + } + + { + // Two element 2D triangle mesh + + // The exact coordinates should not matter as the conversion is an entirely + // topological conversion + let vertices = vec![Point2::::origin(); 4]; + let connectivity = vec![Tri3d2Connectivity([0, 1, 2]), Tri3d2Connectivity([2, 3, 0])]; + let mesh = Mesh::from_vertices_and_connectivity(vertices.clone(), connectivity); + let polymesh = PolyMesh::from(&mesh); + + assert_eq!(polymesh.vertices(), vertices.as_slice()); + assert_eq!(polymesh.num_cells(), 2); + assert_eq!(polymesh.num_faces(), 5); + + verify_connected_poly_mesh(&mesh, &polymesh); + } + + { + // 2D quad mesh + let mesh = create_rectangular_uniform_quad_mesh_2d(1.0, 3, 4, 1, &Vector2::zeros()); + let polymesh = PolyMesh::from(&mesh); + + verify_connected_poly_mesh(&mesh, &polymesh); + } + + { + // 2D triangle mesh + let mesh = create_rectangular_uniform_quad_mesh_2d(1.0, 3, 4, 1, &Vector2::zeros()); + let mesh = mesh.split_into_triangles(); + let polymesh = PolyMesh::from(&mesh); + + verify_connected_poly_mesh(&mesh, &polymesh); + } + + { + // 3D hex mesh, single hexahedron + let hex_mesh = create_rectangular_uniform_hex_mesh(1.0, 1, 1, 1, 1); + let polymesh = PolyMesh::from(&hex_mesh); + + verify_connected_poly_mesh(&hex_mesh, &polymesh); + } + + { + // 3D hex mesh + let hex_mesh = create_rectangular_uniform_hex_mesh(1.0, 2, 2, 1, 1); + let polymesh = PolyMesh::from(&hex_mesh); + + verify_connected_poly_mesh(&hex_mesh, &polymesh); + } + + { + // 3D hex mesh, bigger/more complex + let hex_mesh = create_rectangular_uniform_hex_mesh(1.0, 3, 2, 4, 1); + let polymesh = PolyMesh::from(&hex_mesh); + + verify_connected_poly_mesh(&hex_mesh, &polymesh); + } +} + +proptest! { + #[test] + fn keep_cells_correctly_preserves_cells_for_quad_mesh( + (mesh, cell_indices) in rectangular_uniform_mesh_strategy(1.0, 4) + // For each mesh, generate a strategy that outputs the mesh itself + // and a collection of cell indices which are in bounds with respect to the mesh + .prop_flat_map( + |mesh| { + let vec_size_range = (0, max(1, 2 * mesh.connectivity().len())); + let vec_element_strategy = 0..max(1, mesh.connectivity().len()); + (Just(mesh), vec(vec_element_strategy, vec_size_range)) + } + ) + ) { + // For every mesh and an appropriate selection of cells to keep, + // it must hold that the kept cells are equivalent in the sense that + // the points that define the cell are the same. + let new_mesh = mesh.keep_cells(&cell_indices); + + let kept_quads_from_old_mesh: Vec<_> = cell_indices + .iter() + .cloned() + .map(|old_cell_index| mesh.connectivity()[old_cell_index]) + .map(|connectivity| connectivity.cell(mesh.vertices()).unwrap()) + .collect(); + + let kept_quads_from_new_mesh: Vec<_> = (0 .. new_mesh.connectivity().len()) + .into_iter() + .map(|new_cell_index| new_mesh.connectivity()[new_cell_index]) + .map(|connectivity| connectivity.cell(new_mesh.vertices()).unwrap()) + .collect(); + + prop_assert_eq!(kept_quads_from_old_mesh, kept_quads_from_new_mesh); + } +} diff --git a/fenris/tests/unit_tests/mod.rs b/fenris/tests/unit_tests/mod.rs new file mode 100644 index 0000000..5d9dc95 --- /dev/null +++ b/fenris/tests/unit_tests/mod.rs @@ -0,0 +1,15 @@ +mod assembly; +mod basis; +mod conversion_formulas; +mod cg; +mod element; +mod embedding; +mod fe_mesh; +mod geometry; +mod materials; +mod mesh; +mod polygon; +mod polymesh; +mod polytope; +mod reorder; +mod sparse; diff --git a/fenris/tests/unit_tests/polygon.rs b/fenris/tests/unit_tests/polygon.rs new file mode 100644 index 0000000..91b5223 --- /dev/null +++ b/fenris/tests/unit_tests/polygon.rs @@ -0,0 +1,197 @@ +use fenris::geometry::{GeneralPolygon, LineSegment2d, Orientation, Polygon}; +use matrixcompare::assert_scalar_eq; +use nalgebra::{Point2, Vector2}; + +use crate::assert_approx_matrix_eq; +use std::f64; + +#[test] +fn polygon_area_signed_unsigned() { + // This is a simple, but fairly non-convex polygon oriented counter-clockwise + let vertices = vec![ + Point2::new(-5.0, -2.0), + Point2::new(-3.0, -3.0), + Point2::new(-1.0, 0.0), + Point2::new(-3.0, -1.0), + Point2::new(-5.0, 1.0), + Point2::new(-3.0, 1.0), + Point2::new(-6.0, 3.0), + ]; + + let polygon = GeneralPolygon::from_vertices(vertices.clone()); + + let expected_area = 10.5; + assert_scalar_eq!(polygon.signed_area(), expected_area, comp = abs, tol = 1e-12); + assert_scalar_eq!(polygon.area(), expected_area, comp = abs, tol = 1e-12); + + let vertices_reversed = { + let mut v = vertices.clone(); + v.reverse(); + v + }; + + let reversed_polygon = GeneralPolygon::from_vertices(vertices_reversed); + + assert_scalar_eq!(reversed_polygon.signed_area(), -expected_area, comp = abs, tol = 1e-12); + assert_scalar_eq!(reversed_polygon.area(), expected_area, comp = abs, tol = 1e-12); +} + +#[test] +fn polygon_intersects_segment() { + // This is a simple, but fairly non-convex polygon oriented counter-clockwise + let vertices = vec![ + Point2::new(-5.0, -2.0), + Point2::new(-3.0, -3.0), + Point2::new(-1.0, 0.0), + Point2::new(-3.0, -1.0), + Point2::new(-5.0, 1.0), + Point2::new(-3.0, 1.0), + Point2::new(-6.0, 3.0), + ]; + + let polygon = GeneralPolygon::from_vertices(vertices.clone()); + + { + // Segment is outside (and also outside its convex hull) + let segment = LineSegment2d::new(Point2::new(-8.0, -1.0), Point2::new(-7.0, 3.0)); + assert_eq!(polygon.intersects_segment(&segment), false); + } + + { + // Segment is outside (but inside the convex hull of the polygon) + let segment = LineSegment2d::new(Point2::new(-3.0, 0.0), Point2::new(-2.0, 1.0)); + assert_eq!(polygon.intersects_segment(&segment), false); + } + + { + // Segment is completely inside the polygon + let segment = LineSegment2d::new(Point2::new(-3.0, -2.0), Point2::new(-5.0, 0.0)); + assert_eq!(polygon.intersects_segment(&segment), true); + } + + { + // Segment is partially inside, with one of its endpoints inside + let segment = LineSegment2d::new(Point2::new(-3.0, -2.0), Point2::new(-4.0, 0.5)); + assert_eq!(polygon.intersects_segment(&segment), true); + } + + { + // Segment is partially inside, with none of its endpoints inside + let segment = LineSegment2d::new(Point2::new(0.0, -1.0), Point2::new(-6.0, 0.0)); + assert_eq!(polygon.intersects_segment(&segment), true); + } +} + +#[test] +fn polygon_closest_edge() { + // This is a simple, but fairly non-convex polygon oriented counter-clockwise + let vertices = vec![ + Point2::new(-5.0, -2.0), + Point2::new(-3.0, -3.0), + Point2::new(-1.0, 0.0), + Point2::new(-3.0, -1.0), + Point2::new(-5.0, 1.0), + Point2::new(-3.0, 1.0), + Point2::new(-6.0, 3.0), + ]; + + let polygon = GeneralPolygon::from_vertices(vertices.clone()); + + { + // Point is outside, but inside the convex hull of the polygon + let point = Point2::new(-3.0, 0.0); + let closest_edge = polygon.closest_edge(&point).unwrap(); + let expected_t = f64::sqrt(0.5) / 2.82842712474619; + + assert_eq!(closest_edge.edge_index, 3); + assert_scalar_eq!(closest_edge.signed_distance, f64::sqrt(0.5), comp = abs, tol = 1e-12); + assert_scalar_eq!(closest_edge.edge_parameter, expected_t, comp = abs, tol = 1e-12); + assert_approx_matrix_eq!(closest_edge.edge_point.coords, Vector2::new(-3.5, -0.5), abstol = 1e-12); + } + + { + // Point is exactly on the boundary + let point = Point2::new(-4.5, 1.0); + let closest_edge = polygon.closest_edge(&point).unwrap(); + let expected_t = 0.25; + + assert_eq!(closest_edge.edge_index, 4); + assert_scalar_eq!(closest_edge.signed_distance, 0.0, comp = abs, tol = 1e-12); + assert_scalar_eq!(closest_edge.edge_parameter, expected_t, comp = abs, tol = 1e-12); + assert_approx_matrix_eq!(closest_edge.edge_point.coords, point.coords, abstol = 1e-12); + } + + { + // Point is inside, closest to a vertex + let point = Point2::new(-3.1, -1.4); + let closest_edge = polygon.closest_edge(&point).unwrap(); + + // Whether edge 2 or 3 is reported is not well-defined. It can be either. + assert!([2, 3].contains(&closest_edge.edge_index)); + let expected_t = if closest_edge.edge_index == 2 { 1.0 } else { 0.0 }; + assert_scalar_eq!( + closest_edge.signed_distance, + -0.412310562561766, + comp = abs, + tol = 1e-12 + ); + assert_scalar_eq!(closest_edge.edge_parameter, expected_t, comp = abs, tol = 1e-12); + assert_approx_matrix_eq!(closest_edge.edge_point.coords, vertices[3].coords, abstol = 1e-12); + } + + { + // Point is inside, closest to an edge + let point = Point2::new(-5.0, 0.0); + let closest_edge = polygon.closest_edge(&point).unwrap(); + let expected_t = 0.61538461538; + + assert_eq!(closest_edge.edge_index, 6); + assert_scalar_eq!( + closest_edge.signed_distance, + -0.392232270276368, + comp = abs, + tol = 1e-12 + ); + assert_scalar_eq!(closest_edge.edge_parameter, expected_t, comp = abs, tol = 1e-10); + assert_approx_matrix_eq!( + closest_edge.edge_point.coords, + Vector2::new(-5.384615384615385, -0.076923076923077), + abstol = 1e-12 + ); + } +} + +#[test] +fn polygon_orient() { + // This is a simple, but fairly non-convex polygon oriented counter-clockwise + let vertices = vec![ + Point2::new(-5.0, -2.0), + Point2::new(-3.0, -3.0), + Point2::new(-1.0, 0.0), + Point2::new(-3.0, -1.0), + Point2::new(-5.0, 1.0), + Point2::new(-3.0, 1.0), + Point2::new(-6.0, 3.0), + ]; + let mut polygon = GeneralPolygon::from_vertices(vertices.clone()); + + // Check that area is still the same as a sanity check that we don't accidentally + // completely break the polygon somehow + let area = polygon.area(); + + assert_eq!(polygon.orientation(), Orientation::Counterclockwise); + + polygon.orient(Orientation::Counterclockwise); + assert_eq!(polygon.orientation(), Orientation::Counterclockwise); + + polygon.orient(Orientation::Clockwise); + assert_eq!(polygon.orientation(), Orientation::Clockwise); + assert_scalar_eq!(polygon.area(), area, comp = abs, tol = 1e-14); + // We guarantee that the first vertex is the same + assert_eq!(polygon.vertices()[0], vertices[0]); + + // Changing the orientation back again should completely restore the original polygon + polygon.orient(Orientation::Counterclockwise); + assert_eq!(polygon.orientation(), Orientation::Counterclockwise); + assert_eq!(polygon.vertices(), vertices.as_slice()); +} diff --git a/fenris/tests/unit_tests/polymesh.rs b/fenris/tests/unit_tests/polymesh.rs new file mode 100644 index 0000000..b84b673 --- /dev/null +++ b/fenris/tests/unit_tests/polymesh.rs @@ -0,0 +1,81 @@ +use fenris::geometry::polymesh::{PolyMesh, PolyMesh3d}; +use fenris::geometry::procedural::create_rectangular_uniform_hex_mesh; +use nested_vec::NestedVec; + +use itertools::iproduct; +use matrixcompare::assert_scalar_eq; +use nalgebra::Point3; + +fn create_single_tetrahedron_polymesh() -> PolyMesh3d { + let vertices = vec![ + Point3::new(0.0, 0.0, 0.0), + Point3::new(1.0, 0.0, 0.0), + Point3::new(0.0, 1.0, 0.0), + Point3::new(0.0, 0.0, 1.0), + ]; + let faces = NestedVec::from(&vec![ + // TODO: This isn't consistent with respect to winding order etc. + // We need to introduce the concept of half faces or something similar to + // make this stuff consistent per cell + vec![0, 1, 2], + vec![0, 1, 3], + vec![1, 2, 3], + vec![2, 0, 3], + ]); + let cells = NestedVec::from(&vec![vec![0, 1, 2, 3]]); + PolyMesh::from_poly_data(vertices, faces, cells) +} + +#[test] +fn triangulate_single_tetrahedron_is_unchanged() { + let mesh = create_single_tetrahedron_polymesh(); + + let triangulated = mesh.triangulate().unwrap(); + + assert_eq!(triangulated.num_cells(), 1); + assert_eq!(triangulated.num_faces(), 4); + + // TODO: Further tests! +} + +#[test] +fn compute_volume() { + { + // Single cube, multiple resolutions + let unit_lengths = [1.0, 0.5, 1.5]; + let nx = [1, 2, 3]; + let ny = [1, 2, 3]; + let nz = [1, 2, 3]; + let resolutions = [1, 2]; + + for (u, nx, ny, nz, res) in iproduct!(&unit_lengths, &nx, &ny, &nz, &resolutions) { + let cube = create_rectangular_uniform_hex_mesh(*u, *nx, *ny, *nz, *res); + let cube = PolyMesh3d::from(&cube); + let expected_volume: f64 = u * u * u * (nx * ny * nz) as f64; + dbg!(u, nx, ny, nz, res); + assert_scalar_eq!(cube.compute_volume(), expected_volume, comp = abs, tol = 1e-12); + } + } +} + +#[test] +fn keep_cells() { + { + // Single tetrahedron + let mesh = create_single_tetrahedron_polymesh(); + + // Keep no cells, should give empty mesh + { + let kept = mesh.keep_cells(&[]); + assert_eq!(kept.vertices().len(), 0); + assert_eq!(kept.num_faces(), 0); + assert_eq!(kept.num_cells(), 0); + } + + // Keep cell 0, should give unchanged mesh back + { + let kept = mesh.keep_cells(&[0]); + assert_eq!(mesh, kept); + } + } +} diff --git a/fenris/tests/unit_tests/polytope.rs b/fenris/tests/unit_tests/polytope.rs new file mode 100644 index 0000000..2b570ab --- /dev/null +++ b/fenris/tests/unit_tests/polytope.rs @@ -0,0 +1,245 @@ +use fenris::geometry::{ConvexPolygon, HalfPlane, Line2d, LineSegment2d, Triangle}; + +use nalgebra::{Point2, Unit, Vector2}; + +use matrixcompare::assert_scalar_eq; + +use crate::assert_approx_matrix_eq; + +#[test] +fn half_plane_surface_distance_and_contains_point() { + let x0 = Point2::new(1.0, -1.0); + let n = Unit::new_normalize(Vector2::new(-1.0, 1.0)); + let half_plane = HalfPlane::from_point_and_normal(x0, n); + + { + let x = Point2::new(-1.0, 1.0); + let dist = half_plane.surface_distance_to_point(&x); + let expected = 2.828427124746; + let diff: f64 = dist - expected; + + assert!(diff.abs() < 1e-6); + assert!(half_plane.contains_point(&x)); + } + + { + let x = Point2::new(2.0, 1.0); + let dist = half_plane.surface_distance_to_point(&x); + let expected = 0.7071067811865; + let diff: f64 = dist - expected; + + assert!(diff.abs() < 1e-6); + } +} + +#[test] +fn empty_polygon_intersect_halfplane() { + let x0 = Point2::new(0.5, -1.0); + let n = Unit::new_normalize(Vector2::new(0.3, -2.0)); + let empty = ConvexPolygon::::from_vertices(vec![]); + + let intersection = empty.intersect_halfplane(&HalfPlane::from_point_and_normal(x0, n)); + + assert_eq!(empty, intersection); +} + +#[test] +fn point_polygon_intersect_halfplane() { + let x0 = Point2::new(1.0, -1.0); + let n = Unit::new_normalize(Vector2::new(-1.0, 1.0)); + let half_plane = HalfPlane::from_point_and_normal(x0, n); + + // Point inside of half plane + { + let x = Point2::new(-1.0, 1.0); + let poly = ConvexPolygon::from_vertices(vec![x]); + let intersection = poly.intersect_halfplane(&half_plane); + assert_eq!(intersection, poly); + } + + // Point outside of half plane + { + let x = Point2::new(2.0, -1.0); + let poly = ConvexPolygon::from_vertices(vec![x]); + let intersection = poly.intersect_halfplane(&half_plane); + assert_eq!(intersection, ConvexPolygon::from_vertices(vec![])); + } +} + +#[test] +fn line_polygon_intersect_halfplane() { + let x0 = Point2::new(1.0, -1.0); + let n = Unit::new_normalize(Vector2::new(-1.0, 1.0)); + let half_plane = HalfPlane::from_point_and_normal(x0, n); + + // Line represented as polygon intersecting the surface of the halfplane + { + let x1 = Point2::new(-1.0, 1.0); + let x2 = Point2::new(2.0, -1.0); + let poly = ConvexPolygon::from_vertices(vec![x1, x2]); + let intersection = poly.intersect_halfplane(&half_plane); + let expected = ConvexPolygon::from_vertices(vec![x1, Point2::new(1.4, -0.6)]); + + assert_approx_matrix_eq!(intersection.vertices()[0], expected.vertices()[0], abstol = 1e-6); + assert_approx_matrix_eq!(intersection.vertices()[1], expected.vertices()[1], abstol = 1e-6); + } +} + +#[test] +fn line_line_intersection() { + let line1 = Line2d::from_point_and_dir(Point2::new(0.0, -1.0), Vector2::new(1.0, 1.0).normalize()); + let line2 = Line2d::from_point_and_dir(Point2::new(-2.0, 2.0), Vector2::new(4.0, -2.0).normalize()); + + let intersection = line1.intersect(&line2).expect("Intersection exists"); + + assert_approx_matrix_eq!(intersection, Point2::new(4.0 / 3.0, 1.0 / 3.0), abstol = 1e-6); +} + +#[test] +fn triangle_polygon_intersect_halfplane() { + let triangle = ConvexPolygon::from_vertices(vec![ + Point2::new(0.0, 3.0), + Point2::new(-2.0, 0.0), + Point2::new(1.0, -1.0), + ]); + + let halfplane = + HalfPlane::from_point_and_normal(Point2::new(2.0, 2.0), Unit::new_normalize(Vector2::new(-4.0, 3.0))); + + let intersection = triangle.intersect_halfplane(&halfplane); + + assert_eq!(intersection.vertices().len(), 4); + assert_approx_matrix_eq!(intersection.vertices()[0], Point2::new(0.0, 3.0), abstol = 1e-12); + assert_approx_matrix_eq!(intersection.vertices()[1], Point2::new(-2.0, 0.0), abstol = 1e-12); + assert_approx_matrix_eq!(intersection.vertices()[2], Point2::new(0.0, -2.0 / 3.0), abstol = 1e-12); + assert_approx_matrix_eq!(intersection.vertices()[3], Point2::new(0.6875, 0.25), abstol = 1e-12); +} + +#[test] +#[ignore] +/// TODO: Make this test pass! +fn triangle_intersect_box_vertex_intersection() { + // The vertices of the triangle exactly lie on the edges of the box + let a = Point2::new(1.0, 1.0); + let c = Point2::new(3.0, 0.0); + let d = Point2::new(2.0, -2.0); + let triangle_poly = ConvexPolygon::from_vertices(vec![d, c, a]); + let box_poly = ConvexPolygon::from_vertices(vec![ + Point2::new(0.0, -2.0), + Point2::new(3.0, -2.0), + Point2::new(3.0, 1.0), + Point2::new(0.0, 1.0), + ]); + + let intersection = box_poly.intersect_polygon(&triangle_poly); + dbg!(&intersection); + assert_eq!(intersection.vertices().len(), 3); +} + +#[test] +fn triangle_triangle_intersection() { + let triangle1 = ConvexPolygon::from_vertices(vec![ + Point2::new(0.0, 3.0), + Point2::new(-2.0, 0.0), + Point2::new(1.0, -1.0), + ]); + + let triangle2 = ConvexPolygon::from_vertices(vec![ + Point2::new(-2.0, 1.0), + Point2::new(-1.0, -1.0), + Point2::new(2.0, 2.0), + ]); + + let intersection = triangle1.intersect_polygon(&triangle2); + + assert_eq!(intersection.vertices().len(), 6); + let v = intersection.vertices(); + assert_approx_matrix_eq!(v[0], Point2::new(-1.2, 1.2), abstol = 1e-12); + assert_approx_matrix_eq!(v[1], Point2::new(-1.714285714285714, 0.428571428571429), abstol = 1e-12); + assert_approx_matrix_eq!(v[2], Point2::new(-1.4, -0.2), abstol = 1e-12); + assert_approx_matrix_eq!(v[3], Point2::new(-0.5, -0.5), abstol = 1e-12); + assert_approx_matrix_eq!(v[4], Point2::new(0.6, 0.6), abstol = 1e-12); + assert_approx_matrix_eq!(v[5], Point2::new(0.352941176470588, 1.588235294117647), abstol = 1e-12); +} + +#[test] +fn triangulate() { + let a = Point2::new(2.0, 0.0); + let b = Point2::new(6.0, 4.0); + let c = Point2::new(4.0, 6.0); + let d = Point2::new(1.0, 5.0); + let e = Point2::new(1.0, 2.0); + + { + // Empty + let poly = ConvexPolygon::::from_vertices(Vec::new()); + assert!(poly.triangulate_into_vec().is_empty()); + } + + { + // Point + let poly = ConvexPolygon::from_vertices(vec![a]); + assert!(poly.triangulate_into_vec().is_empty()); + } + + { + // Line segment + let poly = ConvexPolygon::from_vertices(vec![a, b]); + assert!(poly.triangulate_into_vec().is_empty()); + } + + { + // Triangle + let poly = ConvexPolygon::from_vertices(vec![a, b, c]); + assert_eq!(poly.triangulate_into_vec(), vec![Triangle([a, b, c])]); + } + + { + // Quad + let poly = ConvexPolygon::from_vertices(vec![a, b, c, d]); + assert_eq!( + poly.triangulate_into_vec(), + vec![Triangle([a, b, c]), Triangle([a, c, d])] + ); + } + + { + // Pentagon + let poly = ConvexPolygon::from_vertices(vec![a, b, c, d, e]); + assert_eq!( + poly.triangulate_into_vec(), + vec![Triangle([a, b, c]), Triangle([a, c, d]), Triangle([a, d, e])] + ) + } +} + +#[test] +fn line_segment_intersect_segment_parametric() { + let segment1 = LineSegment2d::new(Point2::new(2.0, 3.0), Point2::new(3.0, 0.0)); + let segment2 = LineSegment2d::new(Point2::new(3.0, 1.0), Point2::new(3.0, 4.0)); + assert_eq!(segment1.intersect_segment_parametric(&segment2), None); +} + +#[test] +fn line_segment_intersect_polygon() { + let a = Point2::new(2.0, 3.0); + let b = Point2::new(3.0, 0.0); + let segment = LineSegment2d::new(a, b); + + let polygon = ConvexPolygon::from_vertices(vec![ + Point2::new(0.0, 1.0), + Point2::new(3.0, 1.0), + Point2::new(3.0, 4.0), + Point2::new(0.0, 4.0), + ]); + + let result = segment + .intersect_polygon(&polygon) + .expect("Intersection is not empty"); + let expected_intersection = LineSegment2d::new(Point2::new(2.0, 3.0), Point2::new(8.0 / 3.0, 1.0)); + + // The line segment may be defined in two ways, but its midpoint and length uniquely + // defines its shape + assert_approx_matrix_eq!(result.midpoint(), expected_intersection.midpoint(), abstol = 1e-12); + assert_scalar_eq!(result.length(), expected_intersection.length(), comp = abs, tol = 1e-12); +} diff --git a/fenris/tests/unit_tests/reorder.rs b/fenris/tests/unit_tests/reorder.rs new file mode 100644 index 0000000..4d9e9ac --- /dev/null +++ b/fenris/tests/unit_tests/reorder.rs @@ -0,0 +1,30 @@ +use fenris::reorder::{cuthill_mckee, reverse_cuthill_mckee}; +use fenris::sparse::CsrMatrix; +use nalgebra::DMatrix; + +#[test] +fn cuthill_mckee_basic_examples() { + // Basic example + { + let matrix = DMatrix::from_row_slice(4, 4, &[1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1]); + let pattern = CsrMatrix::from(&matrix).sparsity_pattern(); + let perm = cuthill_mckee(&pattern); + + assert_eq!(perm.perm(), &[1, 3, 0, 2]); + + let mut rcm_expected_perm = perm.clone(); + rcm_expected_perm.reverse(); + assert_eq!(&reverse_cuthill_mckee(&pattern), &rcm_expected_perm); + } + + // Diagonal pattern + // Note that the "standard" CM algorithm + { + let matrix = DMatrix::from_row_slice(4, 4, &[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]); + let pattern = CsrMatrix::from(&matrix).sparsity_pattern(); + let perm = cuthill_mckee(&pattern); + assert_eq!(perm.perm(), &[0, 1, 2, 3]); + } + + // TODO: Property-based tests +} diff --git a/fenris/tests/unit_tests/sparse.proptest-regressions b/fenris/tests/unit_tests/sparse.proptest-regressions new file mode 100644 index 0000000..f5b8f4f --- /dev/null +++ b/fenris/tests/unit_tests/sparse.proptest-regressions @@ -0,0 +1,11 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc eb3d0383b97096d2736865250aa2a1e353e78fcde28ede14d02aa9a9a663cb11 # shrinks to coo = CooMatrix { nrows: 1, ncols: 2, i: [0, 0, 0], j: [1, 0, 0], v: [0, 0, 1] } +cc 6cbf6d9b828de0df85a172e2f926a8663b6443d3261b7c9467cfbf7aded8d761 # shrinks to coo = CooMatrix { nrows: 3, ncols: 3, i: [0, 0, 0], j: [0, 0, 0], v: [0, 0, 0] } +cc 4f059c9ff0161935917bc36eb6493be2a373ca223561aaaf1893d190f96fd902 # shrinks to (coo, x, y, alpha, beta) = (CooMatrix { nrows: 1, ncols: 0, i: [], j: [], v: [] }, [-5, -4, 0, -1, -5, -2], [-1, -3, -5, -3, 0, -3], 1, 4) +cc 6840b2b458601adddbafc37004e0b2703cea45f96e1a6fb387f0b0b88f2ae24c # shrinks to matrices = [CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0], minor_indices: [], minor_dim: 0 }, v: [] }] +cc b4d2d7ee3defa0a79c1bcf1cebe4b12224e827d4f1d7ce248ceb0e9c40797f89 # shrinks to (_, [a, b]) = (CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 4, 7], minor_indices: [0, 1, 2, 3, 0, 2, 3], minor_dim: 4 }, v: [0, 0, 0, 0, 0, 0, 0] }, [CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 2, 4], minor_indices: [2, 3, 1, 3], minor_dim: 4 }, v: [5, 4, 5, 7] }, CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 4, 7, 11, 11], minor_indices: [0, 1, 2, 3, 0, 2, 3, 0, 1, 2, 3], minor_dim: 4 }, v: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }]) diff --git a/fenris/tests/unit_tests/sparse.rs b/fenris/tests/unit_tests/sparse.rs new file mode 100644 index 0000000..1c77c41 --- /dev/null +++ b/fenris/tests/unit_tests/sparse.rs @@ -0,0 +1,535 @@ +use fenris::{CooMatrix, CsrMatrix}; + +use std::ops::Add; + +use nalgebra::{DMatrix, DVector, Vector3}; + +use crate::assert_approx_matrix_eq; +use crate::assert_panics; +use fenris::sparse::{proptest_strategies, spmm_csr, spmm_csr_pattern, spmv_csr, CscMatrix}; +use fenris::util::flatten_vertically; +use fenris::util::proptest::CsrStrategy; +use proptest::collection::vec; +use proptest::prelude::*; +use std::sync::Arc; + +#[test] +fn coo_to_csr_sorted_no_duplicates() { + let mut coo = CooMatrix::new(4, 3); + coo.push(0, 1, 1); + coo.push(2, 0, 2); + coo.push(2, 1, 3); + coo.push(2, 2, 4); + coo.push(3, 2, 5); + + let csr = coo.to_csr(Add::add); + + let expected_csr = CsrMatrix::from_csr_data(4, 3, vec![0, 1, 1, 4, 5], vec![1, 0, 1, 2, 2], vec![1, 2, 3, 4, 5]); + + assert_eq!(csr, expected_csr); +} + +#[test] +fn coo_to_csr_minimal_example() { + let mut coo = CooMatrix::new(1, 2); + coo.push(0, 1, 0); + coo.push(0, 0, 0); + coo.push(0, 0, 1); + + let csr = coo.to_csr(Add::add); + let expected_csr = CsrMatrix::from_csr_data(1, 2, vec![0, 2], vec![0, 1], vec![1, 0]); + + assert_eq!(csr, expected_csr); +} + +#[test] +fn coo_to_csr_sorted_with_duplicates() { + let mut coo = CooMatrix::new(4, 3); + coo.push(0, 1, 1); + coo.push(2, 0, 2); + coo.push(2, 0, -1); + coo.push(2, 1, 3); + coo.push(2, 2, 4); + coo.push(3, 2, 5); + + let csr = coo.to_csr(Add::add); + + let expected_csr = CsrMatrix::from_csr_data(4, 3, vec![0, 1, 1, 4, 5], vec![1, 0, 1, 2, 2], vec![1, 1, 3, 4, 5]); + + assert_eq!(csr, expected_csr); +} + +#[test] +fn coo_to_csr_unsorted_without_duplicates() { + let mut coo = CooMatrix::new(4, 3); + coo.push(3, 2, 5); + coo.push(2, 1, 3); + coo.push(2, 2, 4); + coo.push(0, 1, 1); + coo.push(2, 0, 2); + + let csr = coo.to_csr(Add::add); + + let expected_csr = CsrMatrix::from_csr_data(4, 3, vec![0, 1, 1, 4, 5], vec![1, 0, 1, 2, 2], vec![1, 2, 3, 4, 5]); + + assert_eq!(csr, expected_csr); +} + +#[test] +fn csc_to_dense() { + // Dense matrix: + let dense = DMatrix::from_row_slice(4, 5, &vec![1, 0, 2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 4, 5, 6, 0, 7]); + + let column_offsets = vec![0, 2, 4, 6, 6, 7]; + let row_indices = vec![0, 3, 1, 3, 0, 3, 3]; + let values = vec![1, 4, 3, 5, 2, 6, 7]; + + let csc = CscMatrix::from_csc_data(4, 5, column_offsets, row_indices, values); + assert_eq!(csc.to_dense(), dense); +} + +#[test] +fn spmv() { + let matrix = DMatrix::from_row_slice(4, 3, &vec![1, 0, 2, 0, 0, 0, 3, 4, 0, 5, 6, 0]); + let csr = CsrMatrix::from(&matrix); + + let x = DVector::from_iterator(3, vec![1, 2, 3]); + let mut y = DVector::from_iterator(4, vec![4, 5, 6, 7]); + let alpha = 2; + let beta = -1; + + spmv_csr(beta, &mut y, alpha, &csr, &x); + + let y_expected = DVector::from_iterator(4, vec![10, -5, 16, 27]); + + assert_eq!(y, y_expected); +} + +fn default_csr_i32() -> impl Strategy> { + CsrStrategy::new() + .with_shapes((0usize..5, 0usize..5)) + .with_cols_per_row(0usize..5) + .with_elements(-9i32..9i32) +} + +#[test] +fn csr_from_diagonal() { + let a = CsrMatrix::from_diagonal(&Vector3::new(2.0, -1.0, -4.0)); + + let a_diag: Vec<_> = a.diag_iter().collect(); + let a_expected_diag = vec![2.0, -1.0, -4.0]; + assert_eq!(a.nnz(), 3); + assert_eq!(a.nrows(), a.ncols()); + assert_eq!(a.nrows(), 3); + assert_eq!(a_diag, a_expected_diag); +} + +#[test] +fn scale_rows_cols() { + let a = DMatrix::from_row_slice(3, 3, &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]); + let a = CsrMatrix::from(&a); + + let d = DVector::from_column_slice(&[2.0, -4.0, 3.0]); + + // Scale rows + { + let mut a = a.clone(); + a.scale_rows(&d); + let a_dense = a.build_dense(); + let a_expected = DMatrix::from_row_slice(3, 3, &[2.0, 4.0, 6.0, -16.0, -20.0, -24.0, 21.0, 24.0, 27.0]); + assert_approx_matrix_eq!(&a_dense, &a_expected, abstol = 1e-12); + } + + // Scale cols + { + let mut a = a.clone(); + a.scale_cols(&d); + let a_dense = a.build_dense(); + let a_expected = DMatrix::from_row_slice(3, 3, &[2.0, -8.0, 9.0, 8.0, -20.0, 18.0, 14.0, -32.0, 27.0]); + assert_approx_matrix_eq!(&a_dense, &a_expected, abstol = 1e-12); + } +} + +#[test] +fn csr_row_mut() { + let mut csr = { + let mut coo = CooMatrix::new(4, 4); + coo.push(0, 0, 1); + coo.push(0, 1, 2); + coo.push(0, 3, 3); + coo.push(1, 1, 4); + coo.push(1, 3, 5); + coo.push(2, 0, 6); + coo.push(2, 2, 7); + coo.push(3, 1, 8); + coo.push(3, 2, 9); + coo.to_csr(Add::add) + }; + + // Reads only + { + let row = csr.row_mut(0); + assert_eq!(row.value_at_local_index(0), &1); + assert_eq!(row.value_at_local_index(1), &2); + assert_eq!(row.value_at_local_index(2), &3); + assert_panics!(row.value_at_local_index(3)); + assert_eq!(row.col_at_local_index(0), 0); + assert_eq!(row.col_at_local_index(1), 1); + assert_eq!(row.col_at_local_index(2), 3); + assert_panics!(row.col_at_local_index(3)); + + let row = csr.row_mut(1); + assert_eq!(row.value_at_local_index(0), &4); + assert_eq!(row.value_at_local_index(1), &5); + assert_panics!(row.value_at_local_index(2)); + assert_eq!(row.col_at_local_index(0), 1); + assert_eq!(row.col_at_local_index(1), 3); + assert_panics!(row.col_at_local_index(2)); + + let row = csr.row_mut(2); + assert_eq!(row.value_at_local_index(0), &6); + assert_eq!(row.value_at_local_index(1), &7); + assert_panics!(row.value_at_local_index(2)); + assert_eq!(row.col_at_local_index(0), 0); + assert_eq!(row.col_at_local_index(1), 2); + assert_panics!(row.col_at_local_index(2)); + + let row = csr.row_mut(3); + assert_eq!(row.value_at_local_index(0), &8); + assert_eq!(row.value_at_local_index(1), &9); + assert_panics!(row.value_at_local_index(2)); + assert_eq!(row.col_at_local_index(0), 1); + assert_eq!(row.col_at_local_index(1), 2); + assert_panics!(row.col_at_local_index(2)); + } + + // TODO: More tests +} + +#[test] +fn test_spmm_csr_pattern() { + let a = DMatrix::from_row_slice(4, 5, &[1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1]); + let b = DMatrix::from_row_slice(5, 3, &[1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1]); + let c = DMatrix::from_row_slice(4, 3, &[2, 1, 3, 0, 0, 1, 0, 0, 0, 2, 1, 3]); + + let a_pattern = CsrMatrix::from(&a).sparsity_pattern(); + let b_pattern = CsrMatrix::from(&b).sparsity_pattern(); + let c_pattern = spmm_csr_pattern(&a_pattern, &b_pattern); + + let c_pattern_expected = CsrMatrix::from(&c).sparsity_pattern(); + + assert_eq!(&c_pattern, c_pattern_expected.as_ref()); + assert_eq!(c_pattern.nnz(), 7); +} + +#[test] +fn test_spmm_csr() { + { + let a = DMatrix::from_row_slice(2, 2, &[1, 0, 0, 3]); + let b = DMatrix::from_row_slice(2, 1, &[3, -1]); + let c = DMatrix::from_row_slice(2, 1, &[1, 1]); + + let a = CsrMatrix::from(&a); + let b = CsrMatrix::from(&b); + let mut c = CsrMatrix::from(&c); + + let alpha = 1; + let beta = 0; + + spmm_csr(beta, &mut c, alpha, &a, &b).unwrap(); + + let expected = DMatrix::from_row_slice(2, 1, &[3, -3]); + let expected = CsrMatrix::from(&expected); + + assert_eq!(c, expected); + assert_eq!(c.nnz(), 2); + } + + { + let a = DMatrix::from_row_slice(3, 2, &[1, 0, 0, 0, 2, 0]); + let b = DMatrix::from_row_slice(2, 3, &[3, 0, 1, -1, 1, 0]); + let c = DMatrix::from_row_slice(3, 3, &[3, 0, 1, -3, 3, 0, 6, 0, 2]); + + let a = CsrMatrix::from(&a); + let b = CsrMatrix::from(&b); + let mut c = CsrMatrix::from(&c); + + let alpha = 2; + let beta = 2; + + spmm_csr(beta, &mut c, alpha, &a, &b).unwrap(); + + let expected = DMatrix::from_row_slice(3, 3, &[12, 0, 4, -6, 6, 0, 24, 0, 8]); + let expected = CsrMatrix::from(&expected); + + assert_eq!(c, expected); + assert_eq!(c.nnz(), 6); + } + + // Minimal example found by proptest + { + let c_offsets = vec![0, 0, 3]; + let c_indices = vec![0, 1, 2]; + let c_values = vec![0, 0, 0]; + let mut c = CsrMatrix::from_csr_data(2, 3, c_offsets, c_indices, c_values); + + let a_offsets = vec![0, 0, 2]; + let a_indices = vec![0, 1]; + let a_values = vec![8, 5]; + let a = CsrMatrix::from_csr_data(2, 2, a_offsets, a_indices, a_values); + + let b_offsets = vec![0, 0, 3]; + let b_indices = vec![0, 1, 2]; + let b_values = vec![0, 0, -1]; + let b = CsrMatrix::from_csr_data(2, 3, b_offsets, b_indices, b_values); + + let alpha = 3; + let beta = 2; + + spmm_csr(beta, &mut c, alpha, &a, &b).unwrap(); + + let expected_offsets = vec![0, 0, 3]; + let expected_indices = vec![0, 1, 2]; + let expected_values = vec![0, 0, -15]; + let expected = CsrMatrix::from_csr_data(2, 3, expected_offsets, expected_indices, expected_values); + + assert_eq!(c, expected); + } + + { + let a = DMatrix::from_row_slice(4, 5, &[1, 3, 0, 0, 4, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 1, -2, 0, 0, 2]); + let b = DMatrix::from_row_slice(5, 3, &[2, 0, 1, 0, 0, 5, -1, 0, 0, 0, -4, 0, 1, 3, 1]); + let c = DMatrix::from_row_slice(4, 3, &[2, 1, 3, 0, 0, 1, 0, 0, 0, 2, 1, 3]); + + let a = CsrMatrix::from(&a); + let b = CsrMatrix::from(&b); + let mut c = CsrMatrix::from(&c); + + let alpha = 2; + let beta = 3; + + spmm_csr(beta, &mut c, alpha, &a, &b).unwrap(); + + let expected = DMatrix::from_row_slice(4, 3, &[18, 27, 49, 0, 0, -47, 0, 0, 0, 14, 15, -5]); + let expected = CsrMatrix::from(&expected); + + assert_eq!(c, expected); + assert_eq!(c.nnz(), 7); + } +} + +/// Strategy for generating matrices A, B, C such that C = A * B is a valid expression. +fn spmm_compatible_csr_matrices() -> impl Strategy, [CsrMatrix; 2])> { + let a_strategy = CsrStrategy::new() + .with_shapes((0usize..5, 0usize..5)) + .with_cols_per_row(0usize..7) + .with_elements(-9i32..9); + + a_strategy + .prop_flat_map(|a| { + let b_strategy = CsrStrategy::new() + .with_shapes((Just(a.ncols()), 0usize..5)) + .with_cols_per_row(0usize..7) + .with_elements(-9i32..9); + (Just(a), b_strategy) + }) + .prop_flat_map(|(a, b)| { + let c_pattern = Arc::new(spmm_csr_pattern(&a.sparsity_pattern(), &b.sparsity_pattern())); + + let nnz = c_pattern.nnz(); + let c_strategy = vec(-9i32..9, nnz) + .prop_map(move |values| CsrMatrix::from_pattern_and_values(c_pattern.clone(), values)); + + (c_strategy, [Just(a), Just(b)]) + }) +} + +proptest! { + #[test] + fn coo_csr_identical_dense_representations( + coo in proptest_strategies::coo(4, 4, 10, -5..5) + ) { + let coo_as_dense = coo.build_dense(); + let csr = coo.to_csr(Add::add); + let csr_as_dense = csr.build_dense(); + + prop_assert_eq!(coo_as_dense, csr_as_dense) + } + + #[test] + fn csr_spmv_same_as_dense_gemv( + (coo, x, y, alpha, beta) in ( + proptest_strategies::coo(6, 6, 10, -5..5), + vec(-5..5, 6), + vec(-5..5, 6), + -5..5, + -5..5) + ) { + // TODO: It seems `nalgebra` does not compute the correct gemv result + // for zero-sized matrices. Should make an issue with a reproducible test case! + prop_assume!(coo.nrows() > 0); + prop_assume!(coo.ncols() > 0); + + let x = DVector::from_iterator(coo.ncols(), x.into_iter().take(coo.ncols())); + let y = DVector::from_iterator(coo.nrows(), y.into_iter().take(coo.nrows())); + let dense = coo.build_dense(); + let csr = coo.to_csr(Add::add); + + let mut y_gemv = y.clone(); + y_gemv.gemv(alpha, &dense, &x, beta); + + let mut y_spmv = y.clone(); + spmv_csr(beta, &mut y_spmv, alpha, &csr, &x); + + prop_assert_eq!(&y_spmv, &y_gemv); + } + + #[test] + fn csr_mat_mul_vec_same_as_dense( + (coo, x) in ( + proptest_strategies::coo(6, 6, 10, -5..5), + vec(-5..5, 6)) + ) { + // TODO: It seems `nalgebra` does not compute the correct matrix-vector product + // for zero-sized matrices. Should make an issue with a reproducible test case! + prop_assume!(coo.nrows() > 0); + prop_assume!(coo.ncols() > 0); + + let x = DVector::from_iterator(coo.ncols(), x.into_iter().take(coo.ncols())); + let dense = coo.build_dense(); + let csr = coo.to_csr(Add::add); + + let y_dense = &dense * &x; + let y_csr = &csr * &x; + + prop_assert_eq!(&y_csr, &y_dense); + } + + #[test] + fn csr_to_dense( + csr in CsrStrategy::new() + .with_shapes((0usize..7, 0usize.. 7)) + .with_cols_per_row(0usize..7) + .with_elements(-9i32..9)) + { + println!("{}", csr.build_dense()); + } + + #[test] + fn csr_append_csr_rows( + (csr1, csr2) in (0usize .. 8).prop_flat_map(|cols| { + // Generate pairs of CSR matrices with the same number of columns + let shape_strategy = (0usize .. 6, Just(cols)); + let csr_strategy = CsrStrategy::new().with_shapes(shape_strategy) + .with_cols_per_row(0usize ..= 5) + .with_elements(-9i32..9); + (csr_strategy.clone(), csr_strategy) + })) + { + // Sanity check for test generation + prop_assert_eq!(csr1.ncols(), csr2.ncols()); + + let mut csr_result = csr1.clone(); + csr_result.append_csr_rows(&csr2); + + prop_assert_eq!(csr_result.ncols(), csr1.ncols()); + prop_assert_eq!(csr_result.nrows(), csr1.nrows() + csr2.nrows()); + prop_assert_eq!(csr_result.nnz(), csr1.nnz() + csr2.nnz()); + + // Check that the result agrees with the dense concatenation + let dense1 = csr1.build_dense(); + let dense2 = csr2.build_dense(); + let dense_result = csr_result.build_dense(); + let dense_expected = flatten_vertically(&[dense1, dense2]).unwrap(); + + prop_assert_eq!(dense_result, dense_expected); + } + + #[test] + fn csr_to_csc( + csr in CsrStrategy::new().with_shapes((0usize .. 5, 0usize .. 5)) + .with_cols_per_row(0usize .. 5) + .with_elements(-9i32..9i32) + ) + { + let csc = csr.to_csc(); + + prop_assert_eq!(csc.nrows(), csr.nrows()); + prop_assert_eq!(csc.ncols(), csr.ncols()); + prop_assert_eq!(csc.nnz(), csr.nnz()); + + prop_assert_eq!(csr.build_dense(), csc.to_dense()); + } + + #[test] + fn concat_diagonally(matrices in vec(default_csr_i32(), 0..5)) { + let concatenated = CsrMatrix::concat_diagonally(&matrices); + + let expected_nnz = matrices.iter().map(CsrMatrix::nnz).sum(); + let expected_nrows = matrices.iter().map(CsrMatrix::nrows).sum(); + let expected_ncols = matrices.iter().map(CsrMatrix::ncols).sum(); + + prop_assert_eq!(concatenated.nnz(), expected_nnz); + prop_assert_eq!(concatenated.nrows(), expected_nrows); + prop_assert_eq!(concatenated.ncols(), expected_ncols); + + // TODO: Would be nice to have a method for diagonally concatenating matrices in + // nalgebra, then we could simply compare with the result of the dense concatenation + // and we'd be done + let concat_dense = concatenated.build_dense(); + assert_eq!(concat_dense.nrows(), concatenated.nrows()); + assert_eq!(concat_dense.ncols(), concatenated.ncols()); + + let mut row_offset = 0; + let mut col_offset = 0; + + for matrix in &matrices { + // TODO: It seems as if the .get and .index methods cannot take + // inputs of the form 0 .. 0. Should make an issue about this. + // For now, we work around this by explicitly checking nrows and ncols + if matrix.nrows() > 0 && matrix.ncols() > 0 { + let region = (row_offset .. row_offset + matrix.nrows(), + col_offset .. col_offset + matrix.ncols()); + let concat_slice = concat_dense.index(region); + let matrix_dense = matrix.build_dense(); + + prop_assert_eq!(matrix_dense, concat_slice.clone_owned()); + } + + row_offset += matrix.nrows(); + col_offset += matrix.ncols(); + } + } + + #[test] + fn csr_spmm_matches_dense_results( + (mut c, [a, b]) in spmm_compatible_csr_matrices() + ) + { + let a_dense = a.build_dense(); + let b_dense = b.build_dense(); + let mut c_dense = c.build_dense(); + + let alpha = 3; + let beta = 2; + + c_dense.gemm(alpha, &a_dense, &b_dense, beta); + spmm_csr(beta, &mut c, alpha, &a, &b).expect("Matrices should be compatible by definition"); + + prop_assert_eq!(c.build_dense(), c_dense); + } + + #[test] + fn csr_mul_csr_matches_dense_results( + (_, [a, b]) in spmm_compatible_csr_matrices() + ) + { + let a_dense = a.build_dense(); + let b_dense = b.build_dense(); + let c_dense = &a_dense * &b_dense; + + let c = &a * &b; + + prop_assert_eq!(&c.build_dense(), &c_dense); + } +} diff --git a/fenris/tests/utils/mod.rs b/fenris/tests/utils/mod.rs new file mode 100644 index 0000000..8b62433 --- /dev/null +++ b/fenris/tests/utils/mod.rs @@ -0,0 +1,31 @@ +/// Poor man's approx assertion for matrices +#[macro_export] +macro_rules! assert_approx_matrix_eq { + ($x:expr, $y:expr, abstol = $tol:expr) => {{ + let diff = $x - $y; + + let max_absdiff = diff.abs().max(); + let approx_eq = max_absdiff <= $tol; + + if !approx_eq { + println!("abstol: {}", $tol); + println!("left: {}", $x); + println!("right: {}", $y); + println!("diff: {:e}", diff); + } + assert!(approx_eq); + }}; +} + +#[macro_export] +macro_rules! assert_panics { + ($e:expr) => {{ + use std::panic::catch_unwind; + use std::stringify; + let expr_string = stringify!($e); + let result = catch_unwind(|| $e); + if result.is_ok() { + panic!("assert_panics!({}) failed.", expr_string); + } + }}; +} diff --git a/global_stash/Cargo.toml b/global_stash/Cargo.toml new file mode 100644 index 0000000..05370dc --- /dev/null +++ b/global_stash/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "global_stash" +version = "0.1.0" +authors = ["Fabian Löschner "] +edition = "2018" +publish = false + +[dependencies] +serde = "1.0" +serde_json = "1.0" +thiserror = "1.0" diff --git a/global_stash/src/lib.rs b/global_stash/src/lib.rs new file mode 100644 index 0000000..774e662 --- /dev/null +++ b/global_stash/src/lib.rs @@ -0,0 +1,449 @@ +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::io::Write; +use std::rc::Rc; + +use serde::ser::{Serialize, SerializeMap, Serializer}; +use thiserror::Error; + +// TODO: Rename the crate to something not on crates.io yet. + +thread_local! { + #[doc(hidden)] + pub static STASH: RefCell = RefCell::new(Stash::new()) +} + +/// Tries to open a new scope in the global stash to add a level of nesting. Panics if the scope name is already taken by a key-value pair. +#[macro_export] +macro_rules! stash_scope { + ($name:expr) => { + let _guard = $crate::STASH.with(|s| s.borrow_mut().enter($name)).unwrap(); + }; +} + +/// Tries to open a new scope in the global stash to add a level of nesting. Calls the `?`-operator and returns a `StashError` if not successful. +#[macro_export] +macro_rules! try_stash_scope { + ($name:expr) => { + let _guard = $crate::STASH.with(|s| s.borrow_mut().enter($name))?; + }; +} + +#[doc(hidden)] +pub struct Stash { + root: Rc>, + current: Rc>, +} + +impl Serialize for Stash { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.root.borrow().serialize(serializer) + } +} + +/// Error type used by the crate. +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum StashError { + /// Indicates that a scope with the given name could not be opened because the scope's name is already taken by a value. + #[error("in the current stash scope exists a stashed value with the same key as the requested scope name (`{0}`)")] + ScopeNameTaken(String), + /// Indicates that a variable could not be added because the key value is already taken in the current scope. + #[error("in the current stash scope exists a stashed value with the same key or a nested scope with the same name as the requested key (`{0}`)")] + KeyTaken(String), + /// Indicates that a push operation was not successful because the target variable is not an array. + #[error("the given key `{0}` does not contain a value that is an array")] + NotAnArray(String), +} + +struct Scope { + name: String, + parent: Option>>, + children: Vec>>, + entries: BTreeMap, +} + +impl Serialize for Scope { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(self.children.len() + self.entries.len()))?; + for (k, v) in self.entries.iter() { + map.serialize_entry(k, v)?; + } + for child in self.children.iter().map(|c| c.borrow()) { + map.serialize_entry(&child.name, &*child)?; + } + map.end() + } +} + +impl Scope { + fn new>(name: S, parent: Option>>) -> Self { + Scope { + name: name.as_ref().to_string(), + parent: parent, + children: Vec::new(), + entries: BTreeMap::new(), + } + } + + #[allow(unused)] + fn enter(&mut self) -> Guard { + Guard {} + } + + fn leave(&mut self) {} + + fn name_taken>(&self, key: S) -> bool { + let key = key.as_ref(); + self.children.iter().any(|scope| scope.borrow().name == key) || self.entries.contains_key(key) + } + + fn key_exists>(&self, key: S) -> bool { + let key = key.as_ref(); + self.entries.contains_key(key) + } + + fn insert_or_modify, T: Into, F>(&mut self, key: S, value: T, modifier: F) + where + F: FnMut(&mut serde_json::Value), + { + let key = key.as_ref(); + self.entries + .entry(key.to_string()) + .and_modify(modifier) + .or_insert(value.into()); + } + + fn insert_no_overwrite, T: Into>( + &mut self, + key: S, + value: T, + ) -> Result<(), StashError> { + let key = key.as_ref(); + if !self.name_taken(key) { + self.entries.insert(key.to_string(), value.into()); + Ok(()) + } else { + Err(StashError::KeyTaken(key.to_string())) + } + } + + fn push, T: Into>(&mut self, key: S, value: T) -> Result<(), StashError> { + let key = key.as_ref(); + if !self.key_exists(key) { + self.insert_no_overwrite(key, Vec::::new())?; + } + + let array = self.entries.get_mut(key).unwrap(); + if let Some(array) = array.as_array_mut() { + array.push(value.into()); + Ok(()) + } else { + Err(StashError::NotAnArray(key.to_string())) + } + } + + fn clear_values(&mut self) { + self.entries.clear(); + for child in self.children.iter() { + child.borrow_mut().clear_values(); + } + } +} + +#[doc(hidden)] +pub struct Guard; + +impl Drop for Guard { + fn drop(&mut self) { + STASH.with(|s| s.borrow_mut().leave()); + } +} + +impl Stash { + fn new() -> Self { + let root = Rc::new(RefCell::new(Scope::new("root", None))); + Stash { + root: root.clone(), + current: root, + } + } + + #[allow(unused)] + #[doc(hidden)] + pub fn enter>(&mut self, name: S) -> Result { + let name = name.as_ref(); + let requested_scope = { + // Check if there already exists a child scope with the given name + let existing_scope = self + .current + .borrow() + .children + .iter() + .find(|child| child.borrow().name == name) + .cloned(); + + existing_scope + // If there was no existing scope with the given name found, it has to be created + .or_else(|| { + // First check if there's already a value with the given name as key + if self.current.borrow().name_taken(name) { + return None; + } + + // If not, create a new child scope + let new_scope = Scope::new(name, Some(self.current.clone())); + let child = Rc::new(RefCell::new(new_scope)); + + self.current.borrow_mut().children.push(child.clone()); + + Some(child) + }) + // If there is still no new scope, the name is blocked + .ok_or(StashError::ScopeNameTaken(name.to_string()))? + }; + + let guard = requested_scope.borrow_mut().enter(); + self.current = requested_scope; + + Ok(guard) + } + + fn leave(&mut self) { + let new_current = { + self.current.borrow_mut().leave(); + self.current.borrow().parent.clone().unwrap_or_else(|| { + std::panic!("Tried to leave root node"); + }) + }; + self.current = new_current; + } + + fn insert_or_modify, T: Into, F>(&mut self, key: S, value: T, modifier: F) + where + F: FnMut(&mut serde_json::Value), + { + self.current + .borrow_mut() + .insert_or_modify(key, value, modifier) + } + + fn insert_no_overwrite, T: Into>( + &mut self, + key: S, + value: T, + ) -> Result<(), StashError> { + self.current.borrow_mut().insert_no_overwrite(key, value) + } + + fn push, T: Into>(&mut self, key: S, value: T) -> Result<(), StashError> { + self.current.borrow_mut().push(key, value) + } + + fn clear_values(&mut self) { + self.root.borrow_mut().clear_values(); + } +} + +/// Removes all values from the global stash while leaving the scope structure unaffected. +pub fn clear_values() { + crate::STASH.with(|s| s.borrow_mut().clear_values()) +} + +/// Tries to add the given key-value pair to the global stash, nested into the currently open scope. Panics on failure. +pub fn insert_value, T: Into>(key: S, value: T) { + crate::STASH + .with(|s| s.borrow_mut().insert_no_overwrite(key, value)) + .unwrap(); +} + +/// TODO: Docs +pub fn insert_value_or_modify, T: Into, F>(key: S, value: T, modifier: F) +where + F: FnMut(&mut serde_json::Value), +{ + crate::STASH.with(|s| s.borrow_mut().insert_or_modify(key, value, modifier)); +} + +/// Tries to add the given key-value pair to the global stash, nested into the currently open scope. +pub fn try_insert_value, T: Into>(key: S, value: T) -> Result<(), StashError> { + crate::STASH.with(|s| s.borrow_mut().insert_no_overwrite(key, value)) +} + +/// Tries to push a value to an array with the given name in the currently open scope. Panics on failure. +pub fn push_value, T: Into>(array_name: S, value: T) { + crate::STASH + .with(|s| s.borrow_mut().push(array_name, value)) + .unwrap() +} + +/// Tries to push a value to an array with the given name in the currently open scope. +pub fn try_push_value, T: Into>(array_name: S, value: T) -> Result<(), StashError> { + crate::STASH.with(|s| s.borrow_mut().push(array_name, value)) +} + +/// Serialize the global stash as a `String` of JSON. +pub fn to_string() -> serde_json::Result { + crate::STASH.with(|s| serde_json::to_string(&*s.borrow())) +} + +/// Serialize the global stash as a pretty-printed `String` of JSON. +pub fn to_string_pretty() -> serde_json::Result { + crate::STASH.with(|s| serde_json::to_string_pretty(&*s.borrow())) +} + +/// Convert the global stash into a `serde_json::Value`. +pub fn to_value() -> serde_json::Result { + crate::STASH.with(|s| serde_json::to_value(&*s.borrow())) +} + +/// Serialize the global stash as JSON into the IO stream. +pub fn to_writer(writer: W) -> serde_json::Result<()> +where + W: Write, + T: Serialize, +{ + crate::STASH.with(|s| serde_json::to_writer(writer, &*s.borrow())) +} + +/// Serialize the global stash as pretty-printed JSON into the IO stream. +pub fn to_writer_pretty(writer: W) -> serde_json::Result<()> +where + W: Write, + T: Serialize, +{ + crate::STASH.with(|s| serde_json::to_writer_pretty(writer, &*s.borrow())) +} + +#[cfg(test)] +mod tests { + use crate as stash; + + #[test] + fn test_basic() { + let scope_name = "Test scope"; + stash_scope!(scope_name); + + let n_sub_scopes = 5; + for i in 0..n_sub_scopes { + stash_scope!(format!("{}", i)); + assert!(stash::try_insert_value("index", i).is_ok()); + assert!(stash::try_insert_value("square", format!("{} * {} = {}", i, i, i * i)).is_ok()); + } + + for i in 0..n_sub_scopes { + assert!(stash::try_insert_value(format!("{}", i), i).is_err()); + } + + super::STASH.with(|s| { + let s = s.borrow(); + { + let current = s.current.borrow(); + + assert_eq!(¤t.name, scope_name); + assert_eq!(current.children.len(), n_sub_scopes); + for i in 0..n_sub_scopes { + let child = current.children[i].borrow(); + assert_eq!(child.name, format!("{}", i)); + assert_eq!(child.entries.len(), 2); + assert_eq!(child.entries["index"], i); + assert_eq!(child.entries["square"], format!("{} * {} = {}", i, i, i * i)); + } + } + }); + } + + #[test] + fn test_try() { + let scope_name = "Test scope"; + stash_scope!(scope_name); + + { + stash::insert_value("Hello", 0); + let produce_err = || -> Result<(), stash::StashError> { + try_stash_scope!("Hello"); + Ok(()) + }; + let val = produce_err(); + assert_eq!(val, Err(stash::StashError::ScopeNameTaken("Hello".to_string()))); + + let produce_err = || -> Result<(), stash::StashError> { + stash::try_insert_value("Hello", 1)?; + Ok(()) + }; + let val = produce_err(); + assert_eq!(val, Err(stash::StashError::KeyTaken("Hello".to_string()))); + } + } + + #[test] + fn test_serialize() { + stash_scope!("Test scope"); + for i in 0..5 { + stash_scope!(format!("{}", i)); + assert!(stash::try_insert_value("index", i).is_ok()); + assert!(stash::try_insert_value("square", format!("{} * {} = {}", i, i, i * i)).is_ok()); + } + + let json = stash::to_string().unwrap(); + assert_eq!( + &json, + r#"{"Test scope":{"0":{"index":0,"square":"0 * 0 = 0"},"1":{"index":1,"square":"1 * 1 = 1"},"2":{"index":2,"square":"2 * 2 = 4"},"3":{"index":3,"square":"3 * 3 = 9"},"4":{"index":4,"square":"4 * 4 = 16"}}}"# + ); + + stash::clear_values(); + + let json = stash::to_string().unwrap(); + assert_eq!(&json, r#"{"Test scope":{"0":{},"1":{},"2":{},"3":{},"4":{}}}"#); + } + + #[test] + fn test_push() { + let scope_name = "Test scope"; + stash_scope!(scope_name); + + let array_len = 20; + { + stash_scope!("An array"); + for i in 0..array_len { + stash::push_value("arr", i * i); + } + + stash::STASH.with(|s| { + let s = s.borrow(); + let current = s.current.borrow(); + + assert_eq!(current.entries.len(), 1); + assert!(current.entries["arr"].is_array()); + assert_eq!(current.entries["arr"].as_array().unwrap().len(), array_len); + }); + + assert!(stash::try_insert_value("arr2", 0).is_ok()); + assert_eq!( + stash::try_push_value("arr2", 1), + Err(stash::StashError::NotAnArray("arr2".to_string())) + ); + } + } + + #[test] + fn test_array_serialize() { + let scope_name = "Test scope"; + stash_scope!(scope_name); + + { + stash_scope!("An array"); + for i in 0..5 { + stash::push_value("arr", i * i); + } + + let json = stash::to_string().unwrap(); + assert_eq!(&json, r#"{"Test scope":{"An array":{"arr":[0,1,4,9,16]}}}"#); + } + } +} diff --git a/hamilton/.gitignore b/hamilton/.gitignore new file mode 100755 index 0000000..2f88dba --- /dev/null +++ b/hamilton/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock \ No newline at end of file diff --git a/hamilton/Cargo.toml b/hamilton/Cargo.toml new file mode 100755 index 0000000..0c8a205 --- /dev/null +++ b/hamilton/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "hamilton" +version = "0.1.0" +authors = ["Andreas Longva "] +edition = "2018" +publish = false + +[dependencies] +serde = { version="1.0", features=["derive"] } +erased-serde = { version="0.3" } +once_cell = "1.2" + +[dev-dependencies] +serde_json = "1.0" \ No newline at end of file diff --git a/hamilton/examples/basic.rs b/hamilton/examples/basic.rs new file mode 100644 index 0000000..30156dd --- /dev/null +++ b/hamilton/examples/basic.rs @@ -0,0 +1,42 @@ +use hamilton::storages::VecStorage; +use hamilton::{register_component, Component, Entity, StorageContainer}; + +use std::error::Error; + +use serde::{Deserialize, Serialize}; + +use serde_json; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub struct TestComponent(pub usize); + +impl Component for TestComponent { + type Storage = VecStorage; +} + +fn main() -> Result<(), Box> { + register_component::()?; + + let container = StorageContainer::new(); + + { + let storage = container.get_storage::>(); + storage.borrow_mut().insert(Entity::new(), TestComponent(0)); + storage.borrow_mut().insert(Entity::new(), TestComponent(1)); + + dbg!(storage.borrow()); + } + + let json = serde_json::to_string_pretty(&container)?; + + println!("{}", json); + + let deserialized_container: StorageContainer = serde_json::from_str(&json)?; + + { + let storage = deserialized_container.get_storage::>(); + dbg!(storage.borrow()); + } + + Ok(()) +} diff --git a/hamilton/src/container.rs b/hamilton/src/container.rs new file mode 100644 index 0000000..638f0d9 --- /dev/null +++ b/hamilton/src/container.rs @@ -0,0 +1,121 @@ +use crate::{BijectiveStorageMut, Component, Entity}; +use std::any::Any; +use std::cell::RefCell; +use std::ops::Deref; +use std::pin::Pin; + +// Make container_serialize a submodule of this module, so that it can still +// access private members of `StorageContainer`, without exposing this to the rest of the +// crate (using e.g. `pub(crate)`). +mod container_serialize; + +pub use container_serialize::{register_factory, register_storage, RegistrationStatus}; + +#[derive(Debug, Default)] +pub struct StorageContainer { + // Note: The "dyn Any" actually contains instances of "RefCell" + storages: RefCell>)>>, +} + +impl StorageContainer { + pub fn new() -> Self { + Self { + storages: RefCell::new(Vec::new()), + } + } + + pub fn get_component_storage(&self) -> &RefCell + where + C: Component, + C::Storage: 'static + Default, + { + self.get_storage::() + } + + pub fn try_get_component_storage(&self) -> Option<&RefCell> + where + C: Component, + C::Storage: 'static, + { + self.try_get_storage::() + } + + pub fn get_storage(&self) -> &RefCell + where + Storage: 'static + Default, + { + if let Some(storage) = self.try_get_storage() { + return storage; + } + + // We didn't find the storage, so make a new one based on default value + let new_storage = Box::pin(RefCell::new(Storage::default())); + let new_storage_ptr = { + // Make sure that we take a pointer to the right type by first explicitly + // generating a reference to it + let new_storage_ref: &RefCell = new_storage.deref(); + new_storage_ref as *const RefCell + }; + self.storages + .borrow_mut() + .push((String::from(std::any::type_name::()), new_storage)); + + // See the above comment for why this is valid, as the same argument applies. + unsafe { &*new_storage_ptr } + } + + pub fn try_get_storage(&self) -> Option<&RefCell> + where + Storage: 'static, + { + for (_, untyped_storage) in self.storages.borrow().iter() { + if let Some(typed) = untyped_storage.downcast_ref::>() { + let typed_ptr = typed as *const RefCell<_>; + + // This is valid because the RefCell is contained inside Pin, + // so as long as the pinned box is never deallocated, the pointer will remain + // valid. We never remove any storages from the vector, so this could only + // happen upon deallocation of the StorageContainer. However, the + // returned reference is scoped to the lifetime of &self, so + // it cannot outlive the container itself. + return Some(unsafe { &*typed_ptr }); + } + } + + // Did not find storage + None + } + + pub fn replace_storage(&mut self, storage: Storage) -> bool + where + Storage: 'static, + { + // Note: Because we take &mut self here, we know that there cannot be any outstanding + // references given out by `get_storage`, and so it's safe to replace + // (and therefore destroy) the given storage + let mut storages = self.storages.borrow_mut(); + + let tag = std::any::type_name::(); + let pinned_storage = Box::pin(RefCell::new(storage)); + + for (existing_tag, existing_storage) in storages.iter_mut() { + if *existing_tag == tag { + *existing_storage = pinned_storage; + return true; + } + } + + // No storage with the same tag found, so simply add it to existing storages + storages.push((String::from(tag), pinned_storage)); + false + } + + pub fn insert_component(&self, id: Entity, component: C) + where + C: Component, + C::Storage: 'static + Default + BijectiveStorageMut, + { + let mut storage = self.get_component_storage::().borrow_mut(); + storage.insert_component(id, component); + } +} diff --git a/hamilton/src/container/container_serialize.rs b/hamilton/src/container/container_serialize.rs new file mode 100644 index 0000000..cacd9cb --- /dev/null +++ b/hamilton/src/container/container_serialize.rs @@ -0,0 +1,182 @@ +use crate::{EntitySerializationMap, Storage, StorageContainer, StorageFactory}; +use once_cell::sync::Lazy; +use serde::de::DeserializeSeed; +use std::any::Any; +use std::cell::RefCell; +use std::collections::HashMap; +use std::error::Error; +use std::fmt; +use std::ops::Deref; +use std::pin::Pin; +use std::sync::Mutex; + +static REGISTRY: Lazy>>> = Lazy::new(|| Mutex::new(HashMap::new())); + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum RegistrationStatus { + /// Indicates that the factory did not already exist in the registry, so it was inserted. + Inserted, + /// Indicates that a factory was already registered for the given typename, but it was + /// replaced by the new factory. + Replaced, +} + +pub fn register_factory(factory: Box) -> Result> { + let mut hash_map = REGISTRY.lock()?; + // TODO: Handle collision, i.e. if the tag has already been registered + if hash_map.insert(factory.storage_tag(), factory).is_some() { + Ok(RegistrationStatus::Replaced) + } else { + Ok(RegistrationStatus::Inserted) + } +} + +pub fn register_storage() -> Result> +where + S: Storage, +{ + let factory = S::new_factory(); + register_factory(factory) +} + +fn look_up_factory(tag: &str, f: impl FnOnce(&dyn StorageFactory) -> R) -> Result> { + let hash_map = REGISTRY.lock().map_err(Box::::from)?; + let factory = hash_map + .get(tag) + .ok_or_else(|| format!("no factory registered for given tag {}", tag))?; + Ok(f(factory.deref())) +} + +// TODO: Naming +struct FactoryWrapper<'a> { + factory: &'a dyn StorageFactory, + id_map: &'a mut EntitySerializationMap, +} + +impl<'a, 'de> serde::de::DeserializeSeed<'de> for FactoryWrapper<'a> { + type Value = Box; + + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let erased_deserializer = &mut ::erase(deserializer); + self.factory + .deserialize_storage(erased_deserializer, self.id_map) + .map_err(serde::de::Error::custom) + } +} + +struct TaggedStorage<'a> { + id_map: &'a mut EntitySerializationMap, +} + +impl<'a, 'b, 'de> DeserializeSeed<'de> for &'b mut TaggedStorage<'a> { + type Value = (String, Pin>); + + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_tuple(2, self) + } +} + +impl<'a, 'b, 'de> serde::de::Visitor<'de> for &'b mut TaggedStorage<'a> { + type Value = (String, Pin>); + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "2-element sequence (tag, storage)") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let tag: String = seq + .next_element()? + .ok_or_else(|| "missing tag in sequence") + .map_err(serde::de::Error::custom)?; + + look_up_factory(&tag, |factory| -> Result<_, Box> { + let wrapper = FactoryWrapper { + factory: factory.deref(), + id_map: self.id_map, + }; + + let storage: Box = seq + .next_element_seed(wrapper)? + .ok_or_else(|| "missing storage in sequence") + .map_err(Box::::from)?; + Ok(Pin::from(storage)) + }) + // First set of errors is error from locking and looking up the factory + .map_err(serde::de::Error::custom)? + .map(|pinned| (tag, pinned)) + // Second set is from the deserialization of the storage + .map_err(serde::de::Error::custom) + } +} + +struct StorageContainerVisitor(EntitySerializationMap); + +impl<'de> serde::de::Visitor<'de> for StorageContainerVisitor { + type Value = StorageContainer; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "sequence of tuples (tag, storage)") + } + + fn visit_seq(mut self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut storages = Vec::new(); + let mut tagged = TaggedStorage { id_map: &mut self.0 }; + + while let Some((tag, storage)) = seq.next_element_seed(&mut tagged)? { + storages.push((tag, storage)); + } + + let container = StorageContainer { + storages: RefCell::new(storages), + }; + + Ok(container) + } +} + +impl<'de> serde::Deserialize<'de> for StorageContainer { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let id_map = EntitySerializationMap::new(); + deserializer.deserialize_seq(StorageContainerVisitor(id_map)) + } +} + +impl serde::Serialize for StorageContainer { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + + let storages = self.storages.borrow(); + let mut seq = serializer.serialize_seq(Some(storages.len()))?; + for (tag, storage) in storages.iter() { + look_up_factory(&tag, |factory| { + let serialize = factory + .serializable_storage(storage.as_ref().get_ref()) + .map_err(serde::ser::Error::custom)?; + seq.serialize_element(&(tag, &serialize)) + }) + // Handle error from factory look-up + .map_err(serde::ser::Error::custom)? + // Handle error from serialization + .map_err(serde::ser::Error::custom)?; + } + seq.end() + } +} diff --git a/hamilton/src/entity.rs b/hamilton/src/entity.rs new file mode 100644 index 0000000..c458b75 --- /dev/null +++ b/hamilton/src/entity.rs @@ -0,0 +1,48 @@ +use crate::{EntityDeserialize, EntitySerializationMap}; +use serde::de::Deserialize; +use std::sync::atomic::{AtomicU64, Ordering}; + +static NEXT_ENTITY: AtomicU64 = AtomicU64::new(0); + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, serde::Serialize)] +pub struct Entity(u64); + +impl Entity { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Entity(NEXT_ENTITY.fetch_add(1, Ordering::SeqCst)) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +pub struct SerializableEntity(u64); + +impl From for SerializableEntity { + fn from(id: Entity) -> Self { + Self(id.0) + } +} + +impl<'de> EntityDeserialize<'de> for Entity { + fn entity_deserialize(deserializer: D, id_map: &mut EntitySerializationMap) -> Result + where + D: serde::Deserializer<'de>, + { + let deserializable = SerializableEntity::deserialize(deserializer)?; + let entity = id_map.deserialize_entity(deserializable); + Ok(entity) + } +} + +impl<'a, 'de> serde::de::DeserializeSeed<'de> for &'a mut EntitySerializationMap { + type Value = Entity; + + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let deserializable = SerializableEntity::deserialize(deserializer)?; + let entity = self.deserialize_entity(deserializable); + Ok(entity) + } +} diff --git a/hamilton/src/generic_factory.rs b/hamilton/src/generic_factory.rs new file mode 100644 index 0000000..d25cf55 --- /dev/null +++ b/hamilton/src/generic_factory.rs @@ -0,0 +1,55 @@ +use crate::{EntityDeserialize, EntitySerializationMap, StorageFactory}; +use erased_serde::Serialize; +use std::any::Any; +use std::cell::RefCell; +use std::error::Error; +use std::marker::PhantomData; + +#[derive(Debug, Default)] +pub struct GenericFactory { + marker: PhantomData, +} + +impl GenericFactory { + pub fn new() -> Self { + Self { marker: PhantomData } + } +} + +// Factory contains no data whatsoever and is therefore entirely safe to pass around across threads +unsafe impl Sync for GenericFactory {} +unsafe impl Send for GenericFactory {} + +impl StorageFactory for GenericFactory +where + for<'de> Storage: 'static + serde::Serialize + EntityDeserialize<'de>, +{ + fn storage_tag(&self) -> String { + std::any::type_name::().to_string() + } + + fn serializable_storage(&self, storage: &dyn Any) -> Result<&dyn Serialize, Box> { + // TODO: Is this actually valid? + storage + .downcast_ref::>() + .map(|storage| storage as *const RefCell) + // I'm not sure if Any actually provides the necessary guarantees for this to be valid. + // In particular, we are extending the lifetime of the returned reference to + // beyond that of the `downcast_ref` method. However, `Any` can only give a reference + // to a concrete object, and the lifetime of this object must be at least as long + // as the shared reference we have to the trait object, and so by converting our + // reference into a pointer, this points to the contained item, which we know + // has the required lifetime. + .map(|ptr| unsafe { &*ptr } as &dyn Serialize) + .ok_or_else(|| Box::from("provided storage is not known to factory")) + } + + fn deserialize_storage( + &self, + deserializer: &mut dyn erased_serde::Deserializer, + id_map: &mut EntitySerializationMap, + ) -> Result, Box> { + let storage = Storage::entity_deserialize(deserializer, id_map)?; + Ok(Box::new(RefCell::new(storage))) + } +} diff --git a/hamilton/src/lib.rs b/hamilton/src/lib.rs new file mode 100755 index 0000000..ff25745 --- /dev/null +++ b/hamilton/src/lib.rs @@ -0,0 +1,94 @@ +use std::any::Any; +use std::collections::HashMap; +use std::error::Error; + +mod entity; +pub use entity::*; + +mod container; +pub use container::*; + +pub mod storages; + +mod generic_factory; +pub use generic_factory::*; + +mod systems; +pub use systems::*; + +pub struct EntitySerializationMap { + map: HashMap, +} + +impl EntitySerializationMap { + fn new() -> Self { + Self { map: HashMap::new() } + } + + pub fn deserialize_entity(&mut self, id: SerializableEntity) -> Entity { + *self.map.entry(id).or_insert_with(Entity::new) + } +} + +pub trait StorageFactory: Send + Sync { + fn storage_tag(&self) -> String; + + fn serializable_storage(&self, storage: &dyn Any) -> Result<&dyn erased_serde::Serialize, Box>; + + fn deserialize_storage( + &self, + deserializer: &mut dyn erased_serde::Deserializer, + id_map: &mut EntitySerializationMap, + ) -> Result, Box>; +} + +pub trait Storage { + fn new_factory() -> Box; +} + +/// Storage that represents a one-to-one (bijective) correspondence between entities and components. +pub trait BijectiveStorage { + // TODO: Move associated type to `Storage`? + type Component; + + fn get_component_for_entity(&self, id: Entity) -> Option<&Self::Component>; +} + +pub trait BijectiveStorageMut: BijectiveStorage { + /// Inserts a component associated with the entity, overwriting any existing component + /// that may already be associated with the given entity. + fn insert_component(&mut self, id: Entity, component: Self::Component); + + fn get_component_for_entity_mut(&mut self, id: Entity) -> Option<&mut Self::Component>; +} + +/// An extension of serde's `Deserialize` that allows deserialization of types containing +/// instances `Entity` (which are not deserializable) +pub trait EntityDeserialize<'de>: Sized { + fn entity_deserialize(deserializer: D, id_map: &mut EntitySerializationMap) -> Result + where + D: serde::Deserializer<'de>; +} + +impl<'de, T> EntityDeserialize<'de> for T +where + T: serde::Deserialize<'de>, +{ + fn entity_deserialize(deserializer: D, _: &mut EntitySerializationMap) -> Result + where + D: serde::Deserializer<'de>, + { + T::deserialize(deserializer) + } +} + +pub trait Component { + type Storage: Storage; +} + +pub fn register_component() -> Result> +where + C: Component, +{ + register_storage::() +} diff --git a/hamilton/src/storages.rs b/hamilton/src/storages.rs new file mode 100644 index 0000000..9c7c361 --- /dev/null +++ b/hamilton/src/storages.rs @@ -0,0 +1,263 @@ +use crate::{ + BijectiveStorage, BijectiveStorageMut, Entity, EntityDeserialize, EntitySerializationMap, GenericFactory, + SerializableEntity, Storage, StorageFactory, +}; +use std::collections::HashMap; + +#[derive(Clone, Debug, serde::Serialize)] +pub struct VecStorage { + components: Vec, + entities: Vec, + lookup_table: HashMap, +} + +// Helper struct to ease implementation of deserialization +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +struct VecStorageCompanion { + components: Vec, + entities: Vec, + lookup_table: HashMap, +} + +impl VecStorageCompanion { + pub fn to_storage(self, id_map: &mut EntitySerializationMap) -> VecStorage { + VecStorage { + components: self.components, + entities: self + .entities + .into_iter() + .map(|id| id_map.deserialize_entity(id)) + .collect(), + lookup_table: self + .lookup_table + .into_iter() + .map(|(id, idx)| (id_map.deserialize_entity(id), idx)) + .collect(), + } + } +} + +/// Stores component in a vector, with a one-to-one relationship between entities and components. +impl VecStorage { + pub fn new() -> Self { + Self { + components: Vec::new(), + entities: Vec::new(), + lookup_table: HashMap::new(), + } + } + + pub fn len(&self) -> usize { + debug_assert_eq!(self.components.len(), self.entities.len()); + self.components.len() + } + + pub fn is_empty(&self) -> bool { + debug_assert_eq!(self.components.is_empty(), self.entities.is_empty()); + self.components.is_empty() + } + + pub fn get_index(&self, id: Entity) -> Option { + self.lookup_table.get(&id).map(usize::to_owned) + } + + pub fn get_component(&self, id: Entity) -> Option<&Component> { + self.components.get(self.get_index(id)?) + } + + pub fn get_component_mut(&mut self, id: Entity) -> Option<&mut Component> { + let index = self.get_index(id)?; + self.components.get_mut(index) + } + + pub fn insert(&mut self, id: Entity, component: Component) -> usize { + let len = self.len(); + let index = *self.lookup_table.entry(id).or_insert_with(|| len); + + if index < self.components.len() { + *self.components.get_mut(index).unwrap() = component; + } else { + self.components.push(component); + self.entities.push(id); + debug_assert_eq!(index + 1, self.components.len()); + } + + index + } + + /// Removes the component associated with the given entity, if it exists. + /// + /// Returns `true` if a component was removed, otherwise `false`. + pub fn remove_entity(&mut self, id: &Entity) -> bool { + if let Some(index) = self.lookup_table.remove(id) { + self.entities.remove(index); + self.components.remove(index); + true + } else { + false + } + } + + pub fn clear(&mut self) { + self.entities.clear(); + self.components.clear(); + self.lookup_table.clear(); + } + + pub fn components(&self) -> &[Component] { + &self.components + } + + pub fn components_mut(&mut self) -> &mut [Component] { + &mut self.components + } + + pub fn entities(&self) -> &[Entity] { + &self.entities + } + + pub fn entity_component_iter(&self) -> impl Iterator { + self.entities.iter().zip(self.components.iter()) + } + + pub fn entity_component_iter_mut(&mut self) -> impl Iterator { + self.entities.iter().zip(self.components.iter_mut()) + } +} + +impl Default for VecStorage { + fn default() -> Self { + Self::new() + } +} + +impl Storage for VecStorage +where + for<'de> Component: 'static + serde::Serialize + serde::Deserialize<'de>, +{ + fn new_factory() -> Box { + Box::new(GenericFactory::::new()) + } +} + +impl Storage for Vec +where + for<'de> T: Clone + 'static + serde::Serialize + serde::Deserialize<'de>, +{ + fn new_factory() -> Box { + Box::new(GenericFactory::::new()) + } +} + +impl BijectiveStorage for VecStorage { + type Component = C; + + fn get_component_for_entity(&self, id: Entity) -> Option<&Self::Component> { + self.components.get(self.get_index(id)?) + } +} + +impl BijectiveStorageMut for VecStorage { + fn insert_component(&mut self, id: Entity, component: Self::Component) { + self.insert(id, component); + } + + fn get_component_for_entity_mut(&mut self, id: Entity) -> Option<&mut Self::Component> { + let index = self.get_index(id)?; + self.components.get_mut(index) + } +} + +impl<'de, Component> EntityDeserialize<'de> for VecStorage +where + Component: serde::Deserialize<'de>, +{ + fn entity_deserialize(deserializer: D, id_map: &mut EntitySerializationMap) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::Deserialize; + let companion = VecStorageCompanion::::deserialize(deserializer)?; + Ok(companion.to_storage(id_map)) + } +} + +#[derive(Debug, Copy, Clone, serde::Serialize)] +pub struct SingletonStorage { + component: Component, +} + +impl SingletonStorage { + pub fn new(component: Component) -> Self { + Self { component } + } + + pub fn get_component(&self) -> &Component { + &self.component + } + + pub fn get_component_mut(&mut self) -> &mut Component { + &mut self.component + } +} + +impl Storage for SingletonStorage +where + for<'de> Component: 'static + serde::Serialize + EntityDeserialize<'de>, +{ + fn new_factory() -> Box { + Box::new(GenericFactory::>::new()) + } +} + +impl<'de, Component> EntityDeserialize<'de> for SingletonStorage +where + Component: EntityDeserialize<'de>, +{ + fn entity_deserialize(deserializer: D, id_map: &mut EntitySerializationMap) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Self { + component: Component::entity_deserialize(deserializer, id_map)?, + }) + } +} + +#[derive(Debug, Copy, Clone, serde::Serialize)] +pub struct ImmutableSingletonStorage { + component: Component, +} + +impl ImmutableSingletonStorage { + pub fn new(component: Component) -> Self { + Self { component } + } + + pub fn get_component(&self) -> &Component { + &self.component + } +} + +impl Storage for ImmutableSingletonStorage +where + for<'de> Component: 'static + serde::Serialize + EntityDeserialize<'de>, +{ + fn new_factory() -> Box { + Box::new(GenericFactory::>::new()) + } +} + +impl<'de, Component> EntityDeserialize<'de> for ImmutableSingletonStorage +where + Component: EntityDeserialize<'de>, +{ + fn entity_deserialize(deserializer: D, id_map: &mut EntitySerializationMap) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Self { + component: Component::entity_deserialize(deserializer, id_map)?, + }) + } +} diff --git a/hamilton/src/systems.rs b/hamilton/src/systems.rs new file mode 100644 index 0000000..1c4140f --- /dev/null +++ b/hamilton/src/systems.rs @@ -0,0 +1,159 @@ +use crate::StorageContainer; +use std::error::Error; +use std::fmt; +use std::fmt::{Debug, Display}; + +pub trait System: Debug + Display { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box>; +} + +/// A system that runs only once and executes its contained closure +pub struct RunOnceSystem +where + F: FnOnce(&StorageContainer) -> Result<(), Box>, +{ + pub closure: Option, + has_run: bool, +} + +/// System that uses a closure to determine if a system should be run +pub struct FilterSystem +where + P: FnMut(&StorageContainer) -> Result>, + S: System, +{ + pub predicate: P, + pub system: S, +} + +/// Wrapper to store a vector of systems that are run in sequence +pub struct SystemCollection(pub Vec>); + +impl Debug for RunOnceSystem +where + F: FnOnce(&StorageContainer) -> Result<(), Box>, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "RunOnceSystem(has_run: {})", self.has_run) + } +} + +impl RunOnceSystem +where + F: FnOnce(&StorageContainer) -> Result<(), Box>, +{ + pub fn new(closure: F) -> Self { + RunOnceSystem { + closure: Some(closure), + has_run: false, + } + } +} + +impl Display for RunOnceSystem +where + F: FnOnce(&StorageContainer) -> Result<(), Box>, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "RunOnceSystem(has_run: {})", self.has_run) + } +} + +impl System for RunOnceSystem +where + F: FnOnce(&StorageContainer) -> Result<(), Box>, +{ + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + if !self.has_run { + let ret = (self + .closure + .take() + .ok_or_else(|| Box::::from("Closure gone"))?)(data)?; + self.has_run = true; + Ok(ret) + } else { + Ok(()) + } + } +} + +impl Debug for FilterSystem +where + P: FnMut(&StorageContainer) -> Result>, + S: System, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Filter({:?})", self.system) + } +} + +impl Display for FilterSystem +where + P: FnMut(&StorageContainer) -> Result>, + S: System, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Filter({})", self.system) + } +} + +impl System for FilterSystem +where + P: FnMut(&StorageContainer) -> Result>, + S: System, +{ + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + if (self.predicate)(data)? { + self.system.run(data) + } else { + Ok(()) + } + } +} + +impl Debug for SystemCollection { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "SystemCollection({:?})", self.0) + } +} + +impl Display for SystemCollection { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut systems = String::new(); + self.0.iter().for_each(|s| { + systems.push_str(&format!("{}, ", s)); + }); + if systems.len() > 2 { + write!(f, "SystemCollection({})", &systems[..systems.len() - 2]) + } else { + write!(f, "SystemCollection()") + } + } +} + +impl System for SystemCollection { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + for s in self.0.iter_mut() { + s.run(data)?; + } + Ok(()) + } +} + +#[derive(Debug, Default)] +pub struct Systems { + systems: Vec>, +} + +impl Systems { + pub fn add_system(&mut self, system: Box) { + self.systems.push(system); + } + + pub fn run_all(&mut self, data: &StorageContainer) -> Result<(), Box> { + for system in &mut self.systems { + system.run(data)?; + } + Ok(()) + } +} diff --git a/hamilton/tests/registration.rs b/hamilton/tests/registration.rs new file mode 100644 index 0000000..db3a558 --- /dev/null +++ b/hamilton/tests/registration.rs @@ -0,0 +1,18 @@ +use hamilton::{register_factory, GenericFactory, RegistrationStatus}; + +#[test] +fn register() { + // Important: registration is global, so we must run this test in a separate binary, + // which we do when we make it a separate integration test + let make_factory = || Box::new(GenericFactory::::default()); + let make_factory2 = || Box::new(GenericFactory::::default()); + + assert_eq!(register_factory(make_factory()).unwrap(), RegistrationStatus::Inserted); + assert_eq!(register_factory(make_factory()).unwrap(), RegistrationStatus::Replaced); + assert_eq!(register_factory(make_factory()).unwrap(), RegistrationStatus::Replaced); + + assert_eq!(register_factory(make_factory2()).unwrap(), RegistrationStatus::Inserted); + assert_eq!(register_factory(make_factory2()).unwrap(), RegistrationStatus::Replaced); + + assert_eq!(register_factory(make_factory()).unwrap(), RegistrationStatus::Replaced); +} diff --git a/hamilton/tests/serialization/mod.rs b/hamilton/tests/serialization/mod.rs new file mode 100644 index 0000000..161cff1 --- /dev/null +++ b/hamilton/tests/serialization/mod.rs @@ -0,0 +1,75 @@ +use hamilton::storages::VecStorage; +use hamilton::{register_component, Component, Entity, StorageContainer}; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Foo(i32); + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Bar(i32); + +impl Component for Foo { + type Storage = VecStorage; +} + +impl Component for Bar { + type Storage = VecStorage; +} + +#[test] +fn json_roundtrip() { + register_component::().unwrap(); + register_component::().unwrap(); + + let container = StorageContainer::default(); + + let id1 = Entity::new(); + let id2 = Entity::new(); + let id3 = Entity::new(); + + { + let mut foo_storage = container.get_component_storage::().borrow_mut(); + foo_storage.insert(id2, Foo(1)); + foo_storage.insert(id1, Foo(2)); + + let mut bar_storage = container.get_component_storage::().borrow_mut(); + bar_storage.insert(id2, Bar(3)); + bar_storage.insert(id3, Bar(4)); + bar_storage.insert(id1, Bar(5)); + } + + let json = serde_json::to_string_pretty(&container).unwrap(); + + // Drop container so that we make sure we don't accidentally reference it later + drop(container); + + let deserialized_container: StorageContainer = serde_json::from_str(&json).unwrap(); + + let foo_storage = deserialized_container + .get_component_storage::() + .borrow(); + let bar_storage = deserialized_container + .get_component_storage::() + .borrow(); + + let foos = foo_storage.components(); + let bars = bar_storage.components(); + + assert_eq!(foos, &[Foo(1), Foo(2)]); + assert_eq!(bars, &[Bar(3), Bar(4), Bar(5)]); + + // We can not directly compare the entities with expected values, since we cannot predict + // what they should be. However, entities only describe relations, and we can therefore + // instead check that the components that shared the same entities still do after + // serialization and deserialization. + let foo_ids = foo_storage.entities(); + let bar_ids = bar_storage.entities(); + + assert_eq!(foo_ids[0], bar_ids[0]); + assert_eq!(foo_ids[1], bar_ids[2]); + + // Assert that the remaining entity is not equal to any of the others + assert_ne!(bar_ids[1], bar_ids[0]); + assert_ne!(bar_ids[1], bar_ids[2]); +} diff --git a/hamilton/tests/unit.rs b/hamilton/tests/unit.rs new file mode 100644 index 0000000..24c06fa --- /dev/null +++ b/hamilton/tests/unit.rs @@ -0,0 +1,4 @@ +//! Unit tests are grouped into a single binary by way of directories that serve +//! as modules. + +mod serialization; diff --git a/hamilton2/Cargo.toml b/hamilton2/Cargo.toml new file mode 100644 index 0000000..6e219fc --- /dev/null +++ b/hamilton2/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "hamilton2" +version = "0.1.0" +authors = ["Fabian Löschner "] +edition = "2018" +publish = false + +[dependencies] +log = "0.4" +itertools = "0.9" +alga = "0.9" +nalgebra = "0.21" +numeric_literals = "0.2" +approx = "0.3" +coarse-prof = "0.2" diff --git a/hamilton2/src/calculus.rs b/hamilton2/src/calculus.rs new file mode 100644 index 0000000..d94f23e --- /dev/null +++ b/hamilton2/src/calculus.rs @@ -0,0 +1,197 @@ +use alga::general::RealField; +use nalgebra::{DMatrix, DVector, DVectorSlice, DVectorSliceMut, Dim, Dynamic, Scalar, Vector, U1}; + +use nalgebra::base::storage::{Storage, StorageMut}; +use numeric_literals::replace_float_literals; +use std::error::Error; + +pub trait VectorFunction +where + T: Scalar, +{ + fn dimension(&self) -> usize; + fn eval_into(&mut self, f: &mut DVectorSliceMut, x: &DVectorSlice); +} + +impl VectorFunction for &mut X +where + T: Scalar, + X: VectorFunction, +{ + fn dimension(&self) -> usize { + X::dimension(self) + } + + fn eval_into(&mut self, f: &mut DVectorSliceMut, x: &DVectorSlice) { + X::eval_into(self, f, x) + } +} + +pub trait DifferentiableVectorFunction: VectorFunction +where + T: Scalar, +{ + fn solve_jacobian_system( + &mut self, + sol: &mut DVectorSliceMut, + x: &DVectorSlice, + rhs: &DVectorSlice, + ) -> Result<(), Box>; +} + +impl DifferentiableVectorFunction for &mut X +where + T: Scalar, + X: DifferentiableVectorFunction, +{ + fn solve_jacobian_system( + &mut self, + sol: &mut DVectorSliceMut, + x: &DVectorSlice, + rhs: &DVectorSlice, + ) -> Result<(), Box> { + X::solve_jacobian_system(self, sol, x, rhs) + } +} + +#[derive(Debug, Clone)] +pub struct VectorFunctionBuilder { + dimension: usize, +} + +#[derive(Debug, Clone)] +pub struct ConcreteVectorFunction { + dimension: usize, + function: F, + jacobian_solver: J, +} + +impl VectorFunctionBuilder { + pub fn with_dimension(dimension: usize) -> Self { + Self { dimension } + } + + pub fn with_function(self, function: F) -> ConcreteVectorFunction + where + T: Scalar, + F: FnMut(&mut DVectorSliceMut, &DVectorSlice), + { + ConcreteVectorFunction { + dimension: self.dimension, + function, + jacobian_solver: (), + } + } +} + +impl ConcreteVectorFunction { + pub fn with_jacobian_solver(self, jacobian_solver: J) -> ConcreteVectorFunction + where + T: Scalar, + J: FnMut(&mut DVectorSliceMut, &DVectorSlice, &DVectorSlice) -> Result<(), Box>, + { + ConcreteVectorFunction { + dimension: self.dimension, + function: self.function, + jacobian_solver, + } + } +} + +impl VectorFunction for ConcreteVectorFunction +where + T: Scalar, + F: FnMut(&mut DVectorSliceMut, &DVectorSlice), +{ + fn dimension(&self) -> usize { + self.dimension + } + + fn eval_into(&mut self, f: &mut DVectorSliceMut, x: &DVectorSlice) { + let func = &mut self.function; + func(f, x) + } +} + +impl DifferentiableVectorFunction for ConcreteVectorFunction +where + T: Scalar, + F: FnMut(&mut DVectorSliceMut, &DVectorSlice), + J: FnMut(&mut DVectorSliceMut, &DVectorSlice, &DVectorSlice) -> Result<(), Box>, +{ + fn solve_jacobian_system( + &mut self, + sol: &mut DVectorSliceMut, + x: &DVectorSlice, + rhs: &DVectorSlice, + ) -> Result<(), Box> { + let j = &mut self.jacobian_solver; + j(sol, x, rhs) + } +} + +// TODO: Move somewhere else? Ideally contribute as From<_> for DVectorSlice in `nalgebra` +fn as_vector_slice(vector: &Vector) -> DVectorSlice +where + T: Scalar, + S: Storage, + R: Dim, +{ + vector.generic_slice((0, 0), (Dynamic::new(vector.nrows()), U1)) +} + +// TODO: Move somewhere else? Ideally contribute as From<_> for DVectorSliceMut in `nalgebra` +fn as_vector_slice_mut(vector: &mut Vector) -> DVectorSliceMut +where + T: Scalar, + S: StorageMut, + R: Dim, +{ + vector.generic_slice_mut((0, 0), (Dynamic::new(vector.nrows()), U1)) +} + +/// Approximates the Jacobian of a vector function evaluated at `x`, using +/// central finite differences with resolution `h`. +#[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] +pub fn approximate_jacobian(mut f: impl VectorFunction, x: &DVector, h: &T) -> DMatrix +where + T: RealField, +{ + let out_dim = f.dimension(); + let in_dim = x.len(); + + let mut result = DMatrix::zeros(out_dim, in_dim); + + // Define quantities x+ and x- as follows: + // x+ := x + h e_j + // x- := x - h e_j + // where e_j is the jth basis vector consisting of all zeros except for the j-th element, + // which is 1. + let mut x_plus = x.clone(); + let mut x_minus = x.clone(); + + // f+ := f(x+) + // f- := f(x-) + let mut f_plus = DVector::zeros(out_dim); + let mut f_minus = DVector::zeros(out_dim); + + // Use finite differences to compute a numerical approximation of the Jacobian + for j in 0..in_dim { + // TODO: Can optimize this a little by simple resetting the element at the end of the iteration + x_plus.copy_from(x); + x_plus[j] += *h; + x_minus.copy_from(x); + x_minus[j] -= *h; + + f.eval_into(&mut as_vector_slice_mut(&mut f_plus), &as_vector_slice(&x_plus)); + f.eval_into(&mut as_vector_slice_mut(&mut f_minus), &as_vector_slice(&x_minus)); + + // result[.., j] := (f+ - f-) / 2h + let mut column_j = result.column_mut(j); + column_j += &f_plus; + column_j -= &f_minus; + column_j /= 2.0 * *h; + } + + result +} diff --git a/hamilton2/src/dynamic_system/mod.rs b/hamilton2/src/dynamic_system/mod.rs new file mode 100644 index 0000000..15cfce4 --- /dev/null +++ b/hamilton2/src/dynamic_system/mod.rs @@ -0,0 +1,33 @@ +pub(crate) mod mutable; +pub(crate) mod stateless; +pub(crate) mod views; + +pub use mutable::MutableDifferentiableDynamicSystem as DifferentiableDynamicSystem; +pub use mutable::MutableDynamicSystem as DynamicSystem; +pub use stateless::{StatelessDifferentiableDynamicSystem, StatelessDynamicSystem}; +pub use views::IntoStateful; + +// This is old code from exploring an operator based approach to the DynamicSystem trait +/* +mod stateless; +mod stateful; +mod views; + +pub use stateful::{ + DifferentiableDynamicSystemSnapshot, DynamicSystemSnapshot, StatefulDynamicSystem, +}; +pub use stateless::{StatelessDifferentiableDynamicSystem, StatelessDynamicSystem}; +//pub use views::{AsStateful, AsStateless, StatelessView}; +pub use views::StatefulView; + +use nalgebra::{DVectorSlice, DVectorSliceMut, Scalar}; +use std::error::Error; + +pub trait Operator { + /// Applies this operator to `x` and stores or accumulates the result into `y`. + /// + /// It depends on the concrete implementation if the result overwrites the content of `y` + /// or is added to it. + fn apply(&self, y: DVectorSliceMut, x: DVectorSlice) -> Result<(), Box>; +} +*/ diff --git a/hamilton2/src/dynamic_system/mutable.rs b/hamilton2/src/dynamic_system/mutable.rs new file mode 100644 index 0000000..ff102ad --- /dev/null +++ b/hamilton2/src/dynamic_system/mutable.rs @@ -0,0 +1,76 @@ +use nalgebra::{DVectorSlice, DVectorSliceMut, Scalar}; +use std::error::Error; + +/// An abstract dynamic system represented by a second-order ODE. +/// +/// A dynamic system is represented by the second-order system of ODEs +/// +/// ```ignore +/// M dv/dt = f(t, u, v), +/// du/dt = v. +/// ``` +/// +/// where M is constant. +pub trait MutableDynamicSystem { + /// Apply the mass matrix `M` to the vector `x` and accumulate the result in `y`, + /// yielding `y = y + Mx`. + fn apply_mass_matrix(&mut self, y: DVectorSliceMut, x: DVectorSlice); + + fn apply_inverse_mass_matrix( + &mut self, + sol: DVectorSliceMut, + rhs: DVectorSlice, + ) -> Result<(), Box>; + + fn eval_f( + &mut self, + f: DVectorSliceMut, + t: T, + u: DVectorSlice, + v: DVectorSlice, + ) -> Result<(), Box>; +} + +/// An abstract dynamic system that allows for differentiation of the functions involved. +/// +/// This enables implicit integrators to work with an abstract representation of the dynamic +/// system. +pub trait MutableDifferentiableDynamicSystem: MutableDynamicSystem { + /// Internally stores the state, pre-computes stiffness matrix, etc. for calls to Jacobian combination functions + fn set_state(&mut self, t: T, u: DVectorSlice, v: DVectorSlice) -> Result<(), Box>; + + /// Pre-computes the Jacobian linear combination to apply it to any vector using `apply_jacobian_combination` + fn init_apply_jacobian_combination( + &mut self, + alpha: Option, + beta: Option, + gamma: Option, + ) -> Result<(), Box>; + + /// Performs factorization for subsequent calls to `solve_jacobian_combination` + fn init_solve_jacobian_combination(&mut self, alpha: Option, beta: Option) -> Result<(), Box>; + + /// Apply a linear combination of the system Jacobians to a vector. + /// + /// Computes `y = y + (alpha * df/du + beta * df/dv + gamma * M) * x`, + /// with the Jacobians evaluated at time `t` and the provided `u` and `v` variables + /// that were set using `set_state`. + fn apply_jacobian_combination(&mut self, y: DVectorSliceMut, x: DVectorSlice) -> Result<(), Box>; + + /// Solve a system consisting of linear combinations of Jacobians. + /// + /// Specifically, solve a linear system + /// + /// ```ignore + /// H x = rhs, + /// ``` + /// + /// where `H = M + alpha * df/du + beta * df/dv`, + /// with `H` evaluated at time `t` and the provided `u` and `v` variables that were set + /// using `set_state`. + fn solve_jacobian_combination( + &mut self, + sol: DVectorSliceMut, + rhs: DVectorSlice, + ) -> Result<(), Box>; +} diff --git a/hamilton2/src/dynamic_system/stateful.rs b/hamilton2/src/dynamic_system/stateful.rs new file mode 100644 index 0000000..d2f19d2 --- /dev/null +++ b/hamilton2/src/dynamic_system/stateful.rs @@ -0,0 +1,47 @@ +/* +use nalgebra::{DVectorSlice, DVectorSliceMut, Scalar}; +use std::error::Error; + +use crate::dynamic_system::Operator; + +pub trait StatefulDynamicSystem<'snap, T: Scalar> { + type Snapshot: DynamicSystemSnapshot + 'snap; + + fn apply_mass_matrix(&self, y: DVectorSliceMut, x: DVectorSlice); + + fn apply_inverse_mass_matrix( + &self, + sol: DVectorSliceMut, + rhs: DVectorSlice, + ) -> Result<(), Box>; + + fn with_state( + &self, + t: T, + u: DVectorSlice, + v: DVectorSlice, + ) -> Result>; +} + +pub trait DynamicSystemSnapshot { + fn eval_f(&self, f: DVectorSliceMut); +} + +pub trait DifferentiableDynamicSystemSnapshot<'op, T: Scalar>: DynamicSystemSnapshot { + type ApplyJacobianCombinationOperator: Operator + 'op; + type SolveJacobianCombinationOperator: Operator + 'op; + + fn build_apply_jacobian_combination_operator( + &'op self, + alpha: T, + beta: T, + gamma: T, + ) -> Result>; + + fn build_solve_jacobian_combination_operator( + &'op self, + alpha: T, + beta: T, + ) -> Result>; +} +*/ diff --git a/hamilton2/src/dynamic_system/stateless.rs b/hamilton2/src/dynamic_system/stateless.rs new file mode 100644 index 0000000..0fef50f --- /dev/null +++ b/hamilton2/src/dynamic_system/stateless.rs @@ -0,0 +1,65 @@ +use nalgebra::{DVectorSlice, DVectorSliceMut, Scalar}; +use std::error::Error; + +/// An abstract dynamic system represented by a second-order ODE. +/// +/// A dynamic system is represented by the second-order system of ODEs +/// +/// ```ignore +/// M dv/dt = f(t, u, v), +/// du/dt = v. +/// ``` +/// +/// where M is constant. +pub trait StatelessDynamicSystem { + /// Apply the mass matrix `M` to the vector `x` and accumulate the result in `y`, + /// yielding `y = y + Mx`. + fn apply_mass_matrix(&self, y: DVectorSliceMut, x: DVectorSlice); + + fn apply_inverse_mass_matrix(&self, sol: DVectorSliceMut, rhs: DVectorSlice) -> Result<(), Box>; + + fn eval_f(&self, f: DVectorSliceMut, t: T, u: DVectorSlice, v: DVectorSlice); +} + +/// An abstract dynamic system that allows for differentiation of the functions involved. +/// +/// This enables implicit integrators to work with an abstract representation of the dynamic +/// system. +pub trait StatelessDifferentiableDynamicSystem: StatelessDynamicSystem { + /// Apply a linear combination of Jacobians to a vector. + /// + /// Computes `y = y + (alpha * df/du + beta * df/dv + gamma * M) * x`, + /// with the Jacobians evaluated at time `t` and the provided `u` and `v` variables. + fn apply_jacobian_combination( + &self, + y: DVectorSliceMut, + x: DVectorSlice, + t: T, + u: DVectorSlice, + v: DVectorSlice, + alpha: Option, + beta: Option, + gamma: Option, + ) -> Result<(), Box>; + + /// Solve a system consisting of linear combinations of Jacobians. + /// + /// Specifically, solve a linear system + /// + /// ```ignore + /// H x = rhs, + /// ``` + /// + /// where `H = M + alpha * df/du + beta * df/dv`, + /// with `H` evaluated at time `t` and the provided `u` and `v` variables. + fn solve_jacobian_combination( + &self, + sol: DVectorSliceMut, + rhs: DVectorSlice, + t: T, + u: DVectorSlice, + v: DVectorSlice, + alpha: Option, + beta: Option, + ) -> Result<(), Box>; +} diff --git a/hamilton2/src/dynamic_system/views.rs b/hamilton2/src/dynamic_system/views.rs new file mode 100644 index 0000000..76a2253 --- /dev/null +++ b/hamilton2/src/dynamic_system/views.rs @@ -0,0 +1,410 @@ +use nalgebra::{DVector, DVectorSlice, DVectorSliceMut, Scalar}; +use std::error::Error; + +use crate::dynamic_system::mutable::{MutableDifferentiableDynamicSystem, MutableDynamicSystem}; +use crate::dynamic_system::stateless::{StatelessDifferentiableDynamicSystem, StatelessDynamicSystem}; + +/// Wrapper for a `StatelessDynamicSystem` to use it as a `DynamicSystem`, use the `IntoStateful` trait to construct. +pub struct StatefulWrapper { + system: S, + state: Option>, + jacobian_apply_params: Option<(Option, Option, Option)>, + jacobian_solve_params: Option<(Option, Option)>, +} + +/// Allows to view a stateless dynamic system as a stateful system for a simplified implementation. +pub trait IntoStateful { + fn into_stateful(self) -> StatefulWrapper; +} + +impl IntoStateful for S +where + T: Scalar, + S: StatelessDynamicSystem, +{ + fn into_stateful(self) -> StatefulWrapper { + StatefulWrapper { + system: self, + state: None, + jacobian_apply_params: None, + jacobian_solve_params: None, + } + } +} + +struct State { + t: T, + u: DVector, + v: DVector, +} + +impl MutableDynamicSystem for StatefulWrapper +where + T: Scalar, + S: StatelessDynamicSystem, +{ + fn apply_mass_matrix(&mut self, y: DVectorSliceMut, x: DVectorSlice) { + self.system.apply_mass_matrix(y, x) + } + + fn apply_inverse_mass_matrix( + &mut self, + sol: DVectorSliceMut, + rhs: DVectorSlice, + ) -> Result<(), Box> { + self.system.apply_inverse_mass_matrix(sol, rhs) + } + + fn eval_f( + &mut self, + f: DVectorSliceMut, + t: T, + u: DVectorSlice, + v: DVectorSlice, + ) -> Result<(), Box> { + self.system.eval_f(f, t, u, v); + Ok(()) + } +} + +impl MutableDifferentiableDynamicSystem for StatefulWrapper +where + T: Scalar, + S: StatelessDifferentiableDynamicSystem, +{ + fn set_state(&mut self, t: T, u: DVectorSlice, v: DVectorSlice) -> Result<(), Box> { + self.state = Some(State { + t, + u: u.clone_owned(), + v: v.clone_owned(), + }); + + Ok(()) + } + + fn init_apply_jacobian_combination( + &mut self, + alpha: Option, + beta: Option, + gamma: Option, + ) -> Result<(), Box> { + self.jacobian_apply_params = Some((alpha, beta, gamma)); + Ok(()) + } + + fn init_solve_jacobian_combination(&mut self, alpha: Option, beta: Option) -> Result<(), Box> { + self.jacobian_solve_params = Some((alpha, beta)); + Ok(()) + } + + fn apply_jacobian_combination(&mut self, y: DVectorSliceMut, x: DVectorSlice) -> Result<(), Box> { + match (self.state.as_ref(), self.jacobian_apply_params.as_ref()) { + (Some(state), Some((alpha, beta, gamma))) => self.system.apply_jacobian_combination( + y, + x, + state.t.clone(), + DVectorSlice::from(&state.u), + DVectorSlice::from(&state.v), + alpha.clone(), + beta.clone(), + gamma.clone(), + ), + (None, Some(_)) => Err(Box::::from("no state set")), + (Some(_), None) => Err(Box::::from("no jacobian apply params set")), + (None, None) => Err(Box::::from("no state and no jacobian apply params set")), + } + } + + fn solve_jacobian_combination( + &mut self, + sol: DVectorSliceMut, + rhs: DVectorSlice, + ) -> Result<(), Box> { + match (self.state.as_ref(), self.jacobian_solve_params.as_ref()) { + (Some(state), Some((alpha, beta))) => self.system.solve_jacobian_combination( + sol, + rhs, + state.t.clone(), + DVectorSlice::from(&state.u), + DVectorSlice::from(&state.v), + alpha.clone(), + beta.clone(), + ), + (None, Some(_)) => Err(Box::::from("no state set")), + (Some(_), None) => Err(Box::::from("no jacobian solve params set")), + (None, None) => Err(Box::::from("no state and no jacobian solve params set")), + } + } +} + +/* +use std::marker::PhantomData; + +use crate::dynamic_system::DifferentiableDynamicSystemSnapshot; +use crate::dynamic_system::{ + DynamicSystemSnapshot, Operator, StatefulDynamicSystem, StatelessDifferentiableDynamicSystem, + StatelessDynamicSystem, +}; + +/// Allows to view a stateful dynamic system as a stateless system for a simplified interface. +pub struct StatelessView<'a, T, O, S> { + system: &'a S, + _phantom: PhantomData<(T, O)>, +} + +pub trait AsStateless { + fn as_stateless(&self) -> StatelessView; +} + +impl AsStateless for S +where + T: Scalar, + O: DynamicSystemSnapshot, + S: StatefulDynamicSystem, +{ + fn as_stateless(&self) -> StatelessView { + StatelessView { + system: self, + _phantom: PhantomData, + } + } +} + +/// Allows to view a stateless dynamic system as a stateful system for a simplified implementation. +pub struct StatefulView<'view, T, S: 'view> { + system: &'view S, + _phantom: PhantomData, +} + +pub trait AsStateful<'view, 'sys: 'view, T, S: 'view> { + fn as_stateful(&'sys self) -> StatefulView<'view, T, S>; +} + +impl<'view, 'sys: 'view, T, S> AsStateful<'view, 'sys, T, S> for S +where + T: Scalar, + S: StatelessDynamicSystem + 'view, +{ + fn as_stateful(&'sys self) -> StatefulView<'view, T, S> { + StatefulView { + system: self, + _phantom: PhantomData, + } + } +} + +// FIXME: Ideally, u and v would be slices +pub struct NaiveSnapshot<'snap, T: Scalar, S: 'snap> { + system: &'snap S, + t: T, + u: DVector, + v: DVector, +} + +impl<'snap, T, S> DynamicSystemSnapshot for NaiveSnapshot<'snap, T, S> +where + T: Scalar, + S: StatelessDynamicSystem + 'snap, +{ + fn eval_f(&self, f: DVectorSliceMut) { + self.system.eval_f( + f, + self.t.clone(), + DVectorSlice::from(&self.u), + DVectorSlice::from(&self.v), + ); + } +} + +pub struct NaiveApplyJacobianOperator<'op, 'snap: 'op, T: Scalar, S> { + snapshot: &'op NaiveSnapshot<'snap, T, S>, + alpha: T, + beta: T, + gamma: T, +} + +impl<'op, 'snap: 'op, T, S> Operator for NaiveApplyJacobianOperator<'op, 'snap, T, S> +where + T: Scalar, + S: StatelessDifferentiableDynamicSystem + 'snap, +{ + fn apply(&self, y: DVectorSliceMut, x: DVectorSlice) -> Result<(), Box> { + self.snapshot.system.apply_jacobian_combination( + y, + x, + self.snapshot.t.clone(), + DVectorSlice::from(&self.snapshot.u), + DVectorSlice::from(&self.snapshot.v), + self.alpha.clone(), + self.beta.clone(), + self.gamma.clone(), + ) + } +} + +pub struct NaiveSolveJacobianOperator<'op, 'snap: 'op, T: Scalar, S> { + snapshot: &'op NaiveSnapshot<'snap, T, S>, + alpha: T, + beta: T, +} + +impl<'op, 'snap: 'op, T, S> Operator for NaiveSolveJacobianOperator<'op, 'snap, T, S> +where + T: Scalar, + S: StatelessDifferentiableDynamicSystem + 'snap, +{ + fn apply(&self, y: DVectorSliceMut, x: DVectorSlice) -> Result<(), Box> { + self.snapshot.system.solve_jacobian_combination( + y, + x, + self.snapshot.t.clone(), + DVectorSlice::from(&self.snapshot.u), + DVectorSlice::from(&self.snapshot.v), + self.alpha.clone(), + self.beta.clone(), + ) + } +} + +impl<'op, 'snap: 'op, T, S> DifferentiableDynamicSystemSnapshot<'op, T> + for NaiveSnapshot<'snap, T, S> +where + T: Scalar, + S: StatelessDifferentiableDynamicSystem + 'snap, +{ + type ApplyJacobianCombinationOperator = NaiveApplyJacobianOperator<'op, 'snap, T, S>; + type SolveJacobianCombinationOperator = NaiveSolveJacobianOperator<'op, 'snap, T, S>; + + fn build_apply_jacobian_combination_operator( + &'op self, + alpha: T, + beta: T, + gamma: T, + ) -> Result> { + Ok(NaiveApplyJacobianOperator { + snapshot: self, + alpha: alpha.clone(), + beta: beta.clone(), + gamma: gamma.clone(), + }) + } + + fn build_solve_jacobian_combination_operator( + &'op self, + alpha: T, + beta: T, + ) -> Result> { + Ok(NaiveSolveJacobianOperator { + snapshot: self, + alpha: alpha.clone(), + beta: beta.clone(), + }) + } +} + +impl<'snap, 'view: 'snap, T, S> StatefulDynamicSystem<'snap, T> for StatefulView<'view, T, S> +where + T: Scalar, + S: StatelessDynamicSystem + 'view, +{ + type Snapshot = NaiveSnapshot<'snap, T, S>; + + fn apply_mass_matrix(&self, y: DVectorSliceMut, x: DVectorSlice) { + self.system.apply_mass_matrix(y, x); + } + + fn apply_inverse_mass_matrix( + &self, + sol: DVectorSliceMut, + rhs: DVectorSlice, + ) -> Result<(), Box> { + self.system.apply_inverse_mass_matrix(sol, rhs) + } + + fn with_state( + &self, + t: T, + u: DVectorSlice, + v: DVectorSlice, + ) -> Result> { + Ok(NaiveSnapshot { + system: self.system, + t: t.clone(), + u: u.clone_owned(), + v: v.clone_owned(), + }) + } +} +*/ + +/* +impl<'a, T, S, O> StatelessDynamicSystem for StatelessView<'a, T, O, S> +where + T: Scalar, + S: StatefulDynamicSystem, + O: DynamicSystemSnapshot, +{ + fn apply_mass_matrix(&self, y: DVectorSliceMut, x: DVectorSlice) { + self.system.apply_mass_matrix(y, x); + } + + fn apply_inverse_mass_matrix( + &self, + sol: DVectorSliceMut, + rhs: DVectorSlice, + ) -> Result<(), Box> { + self.system.apply_inverse_mass_matrix(sol, rhs) + } + + fn eval_f( + &self, + f: DVectorSliceMut, + t: T, + u: DVectorSlice, + v: DVectorSlice, + ) -> Result<(), Box> { + self.system.with_state(t, u, v)?.eval_f(f); + Ok(()) + } +} + +impl<'a, T, S, O> StatelessDifferentiableDynamicSystem for StatelessView<'a, T, O, S> +where + T: Scalar, + S: StatefulDynamicSystem, + O: DifferentiableDynamicSystemSnapshot, +{ + fn apply_jacobian_combination( + &self, + y: DVectorSliceMut, + x: DVectorSlice, + t: T, + u: DVectorSlice, + v: DVectorSlice, + alpha: T, + beta: T, + gamma: T, + ) -> Result<(), Box> { + self.system + .with_state(t, u, v)? + .build_apply_jacobian_combination_operator(alpha, beta, gamma)? + .apply(y, x) + } + + fn solve_jacobian_combination( + &self, + sol: DVectorSliceMut, + rhs: DVectorSlice, + t: T, + u: DVectorSlice, + v: DVectorSlice, + alpha: T, + beta: T, + ) -> Result<(), Box> { + self.system + .with_state(t, u, v)? + .build_solve_jacobian_combination_operator(alpha, beta)? + .apply(sol, rhs) + } +} +*/ diff --git a/hamilton2/src/integrators/euler.rs b/hamilton2/src/integrators/euler.rs new file mode 100644 index 0000000..653dc32 --- /dev/null +++ b/hamilton2/src/integrators/euler.rs @@ -0,0 +1,332 @@ +use nalgebra::{DVector, DVectorSlice, DVectorSliceMut, RealField, Scalar}; +use numeric_literals::replace_float_literals; +use std::error::Error; + +use crate::dynamic_system::{DifferentiableDynamicSystem, DynamicSystem}; + +use crate::calculus::{DifferentiableVectorFunction, VectorFunction}; +use crate::newton::{newton_line_search, BacktrackingLineSearch, NewtonSettings}; + +#[replace_float_literals(T::from_f64(literal).unwrap())] +fn symplectic_euler_step_( + system: &mut impl DynamicSystem, + mut u: DVectorSliceMut, + mut v: DVectorSliceMut, + mut f: DVectorSliceMut, + mut dv: DVectorSliceMut, + t0: T, + dt: T, +) -> Result<(), Box> +where + T: RealField, +{ + // f <- f(t^n, u^n, v^n) + system.eval_f( + DVectorSliceMut::from(&mut f), + t0, + DVectorSlice::from(&u), + DVectorSlice::from(&v), + )?; + // dv <- M^{-1} * f + system.apply_inverse_mass_matrix(DVectorSliceMut::from(&mut dv), DVectorSlice::from(f))?; + // v += dt * dv + v.axpy(dt, &dv, 1.0); + // u += dt * v + u.axpy(dt, &v, 1.0); + Ok(()) +} + +pub fn symplectic_euler_step<'a, 'b, 'c, 'd, T>( + system: &mut impl DynamicSystem, + u: impl Into>, + v: impl Into>, + f: impl Into>, + dv: impl Into>, + t0: T, + dt: T, +) -> Result<(), Box> +where + T: RealField, +{ + symplectic_euler_step_(system, u.into(), v.into(), f.into(), dv.into(), t0, dt) +} + +/// Implicit function for Backward-Euler. +/// +/// The Backward-Euler discretization is given by +/// M dv - dt * f(t^{n + 1}, u^{n + 1}, v^{n + 1}) = 0 +/// du = dt * v^{n + 1} +/// with dv = v^{n + 1} - v^n, du = u^{n + 1} - u^n +/// +/// Writing w = dv and +/// u^p = u^n + dt * v^n +/// v^{n + 1} = v^n + dv = v^n + w +/// u^{n + 1} = u^n + dt * v^{n + 1} = u^n + dt * v^n + dt * w +/// = u^p + dt * w +/// g(w) = f(t^{n + 1}, u^p + dt * w, v^n + w) +/// We get the non-linear equation system +/// F(w) := M w - dt * g(w) = 0, +/// which we wish to solve with Newton's method. To that end, +/// we need the Jacobian of F. We first note that +/// dg/dw = df/du * du^{n + 1}/dw + df/dv * dv^{n + 1}/dw +/// = df/du * dt * I + df/dv * I +/// = dt * df/du + df/dv +/// dF/dw = M - dt * dg/dw +/// = M - dt * df/dv - (dt)^2 * df/du +struct BackwardEulerImplicitFunction<'a, T, S> +where + T: Scalar, +{ + dynamic_system: &'a mut S, + u_p: DVector, + v_n: DVector, + t_next: T, + dt: T, +} + +impl<'a, T, S> VectorFunction for BackwardEulerImplicitFunction<'a, T, S> +where + T: RealField, + S: DynamicSystem, +{ + fn dimension(&self) -> usize { + self.u_p.len() + } + + #[allow(non_snake_case)] + fn eval_into(&mut self, F: &mut DVectorSliceMut, w: &DVectorSlice) { + let u = &self.u_p + w * self.dt; + let v = &self.v_n + w; + + // F <- - dt * f(t^{n+1}, u, v) + self.dynamic_system + .eval_f( + DVectorSliceMut::from(&mut *F), + self.t_next, + DVectorSlice::from(&u), + DVectorSlice::from(&v), + ) + .unwrap(); + *F *= self.dt * T::from_f64(-1.0).unwrap(); + // F <- F + M * w + self.dynamic_system + .apply_mass_matrix(DVectorSliceMut::from(F), DVectorSlice::from(w)); + } +} + +impl<'a, T, S> DifferentiableVectorFunction for BackwardEulerImplicitFunction<'a, T, S> +where + T: RealField, + S: DifferentiableDynamicSystem, +{ + fn solve_jacobian_system( + &mut self, + sol: &mut DVectorSliceMut, + w: &DVectorSlice, + rhs: &DVectorSlice, + ) -> Result<(), Box> { + // TODO: Avoid allocation + let u = &self.u_p + w * self.dt; + let v = &self.v_n + w; + + self.dynamic_system + .set_state(self.t_next, DVectorSlice::from(&u), DVectorSlice::from(&v))?; + + self.dynamic_system + .init_solve_jacobian_combination(Some(-self.dt * self.dt), Some(-self.dt))?; + + self.dynamic_system + .solve_jacobian_combination(DVectorSliceMut::from(&mut *sol), DVectorSlice::from(&*rhs)) + } +} + +pub struct BackwardEulerSettings { + /// Tolerance for the residual of the dynamic system. + /// + /// More precisely, the tolerance is defined in terms of the inequality + /// norm(M dv/dt - f) <= tol + /// where the derivative dv/dt is approximated numerically and norm( ) is the Euclidean 2-norm. + pub tolerance: T, + pub max_newton_iter: Option, +} + +#[allow(non_snake_case)] +/// Performs one step of the Backward-Euler integrator on the provided dynamic system. +pub fn backward_euler_step<'a, 'b, T>( + system: &mut impl DifferentiableDynamicSystem, + u: impl Into>, + v: impl Into>, + t0: T, + dt: T, + settings: BackwardEulerSettings, +) -> Result> +where + T: RealField, +{ + let mut u = u.into(); + let mut v = v.into(); + + // TODO: Avoid heap-allocating vectors + let dim = u.len(); + let t_next = t0 + dt; + let v_n = v.clone_owned(); + let u_p = &u + &v * dt; + + let F = BackwardEulerImplicitFunction { + dynamic_system: system, + u_p, + v_n, + t_next, + dt, + }; + + // TODO: Avoid allocation + let mut w = DVector::zeros(dim); + let mut f = DVector::zeros(dim); + let mut dw = DVector::zeros(dim); + + let settings = NewtonSettings { + tolerance: dt * settings.tolerance, + max_iterations: settings.max_newton_iter, + }; + + let mut line_search = BacktrackingLineSearch {}; + let iter = newton_line_search(F, &mut w, &mut f, &mut dw, settings, &mut line_search)?; + v += w; + u += &v * dt; + + Ok(iter) +} + +#[cfg(test)] +mod tests { + use crate::calculus::{approximate_jacobian, DifferentiableVectorFunction}; + use crate::dynamic_system::{IntoStateful, StatelessDifferentiableDynamicSystem, StatelessDynamicSystem}; + use crate::integrators::euler::BackwardEulerImplicitFunction; + + use nalgebra::{DMatrix, DVector, DVectorSlice, DVectorSliceMut}; + use std::error::Error; + + /// Mock dynamic system defined by + /// + /// M = some invertible matrix + /// f(t, u, v) = t * dot(v, v) * u + struct MockNonlinearDynamicSystem { + mass: DMatrix, + } + + impl StatelessDynamicSystem for MockNonlinearDynamicSystem { + fn apply_mass_matrix(&self, mut y: DVectorSliceMut, x: DVectorSlice) { + assert!(self.mass.is_square()); + y.gemv(1.0, &self.mass, &x, 1.0); + } + + fn apply_inverse_mass_matrix( + &self, + mut y: DVectorSliceMut, + x: DVectorSlice, + ) -> Result<(), Box> { + let lu = self.mass.clone().lu(); + y.copy_from(&lu.solve(&x).ok_or("LU decomposition failed")?); + Ok(()) + } + + fn eval_f(&self, mut f: DVectorSliceMut, t: f64, u: DVectorSlice, v: DVectorSlice) { + let v_dot_v = v.dot(&v); + f.copy_from(&(t * v_dot_v * u)); + } + } + + impl StatelessDifferentiableDynamicSystem for MockNonlinearDynamicSystem { + fn apply_jacobian_combination( + &self, + _y: DVectorSliceMut, + _x: DVectorSlice, + _t: f64, + _u: DVectorSlice, + _v: DVectorSlice, + _alpha: Option, + _beta: Option, + _gamma: Option, + ) -> Result<(), Box> { + unimplemented!(); + } + + fn solve_jacobian_combination( + &self, + mut sol: DVectorSliceMut, + rhs: DVectorSlice, + t: f64, + u: DVectorSlice, + v: DVectorSlice, + alpha: Option, + beta: Option, + ) -> Result<(), Box> { + let alpha = alpha.unwrap_or(0.0); + let beta = beta.unwrap_or(0.0); + // We have + // f(t, u, v) = t * dot(v, v) * u + // so + // df/du = t * dot(v, v) * I + // df/dv = 2 * t * u * v^T + let dim = u.len(); + let df_du = t * v.dot(&v) * DMatrix::identity(dim, dim); + let df_dv = 2.0 * t * u * v.transpose(); + + let df_comb = &self.mass + alpha * df_du + beta * df_dv; + let lu = df_comb.lu(); + sol.copy_from(&(lu.solve(&rhs).ok_or("LU decomposition failed.")?)); + Ok(()) + } + } + + #[allow(non_snake_case)] + #[test] + fn backward_euler_implicit_function_jacobian() { + // Test that the implicit function defined by + // Backward Euler computes the correct Jacobian by comparing it + // with a numerical approximation of its Jacobian + + let dim = 5; + let t0 = 2.0; + let dt = 2.5; + + let mass_elements = (0..(dim * dim)).into_iter().map(|i| i as f64); + let mass = DMatrix::from_iterator(dim, dim, mass_elements); + + let mut dynamic_system = MockNonlinearDynamicSystem { mass }.into_stateful(); + + let mut F = BackwardEulerImplicitFunction { + dynamic_system: &mut dynamic_system, + u_p: DVector::zeros(dim), + v_n: DVector::zeros(dim), + t_next: t0 + dt, + dt, + }; + + let w_elements = (0..).into_iter().map(|i| i as f64); + let w = DVector::from_iterator(dim, w_elements.clone().take(dim)); + let rhs = DVector::from_iterator(dim, w_elements.skip(dim).take(dim)); + + // We can not directly compute the Jacobian of F(w), since we only + // abstractly have access to the action of the inverse Jacobian. + // However, assuming that the Jacobian is invertible, we note that if A and B + // are both invertible, and + // Ay = b, By = b, + // then A = B provided that b != 0. Thus, it suffices to take some arbitrary + // b and make sure that we get the same solution. + + let mut y_F = DVector::zeros(dim); + F.solve_jacobian_system( + &mut DVectorSliceMut::from(&mut y_F), + &DVectorSlice::from(&w), + &DVectorSlice::from(&rhs), + ) + .unwrap(); + + let j_approx = approximate_jacobian(&mut F, &w.clone_owned(), &1e-6); + let y_approx = j_approx.lu().solve(&rhs).unwrap(); + + assert!(y_F.relative_eq(&y_approx, 1e-6, 1e-12)); + } +} diff --git a/hamilton2/src/integrators/mod.rs b/hamilton2/src/integrators/mod.rs new file mode 100644 index 0000000..1d9dcb1 --- /dev/null +++ b/hamilton2/src/integrators/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod euler; + +pub use euler::{backward_euler_step, symplectic_euler_step, BackwardEulerSettings}; diff --git a/hamilton2/src/lib.rs b/hamilton2/src/lib.rs new file mode 100644 index 0000000..55092b3 --- /dev/null +++ b/hamilton2/src/lib.rs @@ -0,0 +1,12 @@ +#![allow(clippy::excessive_precision)] +#![allow(clippy::too_many_arguments)] + +/// Traits to model dynamic systems that can be integrated by this crate's integrators. +pub mod dynamic_system; +/// Implementations of various integration schemes for dynamic systems. +pub mod integrators; + +/// Calculus helper traits and numerical differentiation +pub mod calculus; +/// Implementations of the Newton method with different line search strategies +pub mod newton; diff --git a/hamilton2/src/newton.rs b/hamilton2/src/newton.rs new file mode 100644 index 0000000..c10af9b --- /dev/null +++ b/hamilton2/src/newton.rs @@ -0,0 +1,248 @@ +use crate::calculus::{DifferentiableVectorFunction, VectorFunction}; +use itertools::iterate; +use log::debug; +use nalgebra::{DVector, DVectorSlice, DVectorSliceMut, RealField, Scalar}; +use numeric_literals::replace_float_literals; +use std::error::Error; +use std::fmt; +use std::fmt::{Display, Formatter}; + +#[derive(Debug, Clone)] +pub struct NewtonResult +where + T: Scalar, +{ + pub solution: DVector, + pub iterations: usize, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct NewtonSettings { + pub max_iterations: Option, + pub tolerance: T, +} + +#[derive(Debug)] +pub enum NewtonError { + /// The procedure failed because the maximum number of iterations was reached. + MaximumIterationsReached(usize), + /// The procedure failed because solving the Jacobian system failed. + JacobianError(Box), + // The line search failed to produce a valid step direction. + LineSearchError(Box), +} + +impl Display for NewtonError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { + match self { + &NewtonError::MaximumIterationsReached(maxit) => { + write!(f, "Failed to converge within maximum number of iterations ({}).", maxit) + } + &NewtonError::JacobianError(ref err) => { + write!(f, "Failed to solve Jacobian system. Error: {}", err) + } + &NewtonError::LineSearchError(ref err) => { + write!(f, "Line search failed to produce valid step direction. Error: {}", err) + } + } + } +} + +impl Error for NewtonError {} + +/// Attempts to solve the non-linear equation F(u) = 0. +/// +/// No heap allocation is performed. The solution is said to have converged if +/// ```|F(u)|_2 <= tolerance```. +/// +/// If successful, returns the number of iterations performed. +#[replace_float_literals(T::from_f64(literal).unwrap())] +pub fn newton<'a, T, F>( + function: F, + x: impl Into>, + f: impl Into>, + dx: impl Into>, + settings: NewtonSettings, +) -> Result +where + T: RealField, + F: DifferentiableVectorFunction, +{ + newton_line_search(function, x, f, dx, settings, &mut NoLineSearch {}) +} + +/// Same as `newton`, but allows specifying a line search. +#[replace_float_literals(T::from_f64(literal).unwrap())] +pub fn newton_line_search<'a, T, F>( + mut function: F, + x: impl Into>, + f: impl Into>, + dx: impl Into>, + settings: NewtonSettings, + line_search: &mut impl LineSearch, +) -> Result +where + T: RealField, + F: DifferentiableVectorFunction, +{ + let mut x = x.into(); + let mut f = f.into(); + let mut minus_dx = dx.into(); + + assert_eq!(x.nrows(), f.nrows()); + assert_eq!(minus_dx.nrows(), f.nrows()); + + function.eval_into(&mut f, &DVectorSlice::from(&x)); + + let mut iter = 0; + + while f.norm() > settings.tolerance { + if settings + .max_iterations + .map(|max_iter| iter == max_iter) + .unwrap_or(false) + { + return Err(NewtonError::MaximumIterationsReached(iter)); + } + + // Solve the system J dx = -f <=> J (-dx) = f + let j_result = function.solve_jacobian_system(&mut minus_dx, &DVectorSlice::from(&x), &DVectorSlice::from(&f)); + if let Err(err) = j_result { + return Err(NewtonError::JacobianError(err)); + } + + // Flip sign to make it consistent with line search + minus_dx *= -1.0; + let dx = &minus_dx; + + let step_length = line_search + .step( + &mut function, + DVectorSliceMut::from(&mut f), + DVectorSliceMut::from(&mut x), + DVectorSlice::from(dx), + ) + .map_err(|err| NewtonError::LineSearchError(err))?; + debug!("Newton step length at iter {}: {}", iter, step_length); + iter += 1; + } + + Ok(iter) +} + +pub trait LineSearch> { + fn step( + &mut self, + function: &mut F, + f: DVectorSliceMut, + x: DVectorSliceMut, + direction: DVectorSlice, + ) -> Result>; +} + +/// Trivial implementation of line search. Equivalent to a single, full Newton step. +#[derive(Clone, Debug)] +pub struct NoLineSearch; + +impl LineSearch for NoLineSearch +where + T: RealField, + F: VectorFunction, +{ + #[replace_float_literals(T::from_f64(literal).unwrap())] + fn step( + &mut self, + function: &mut F, + mut f: DVectorSliceMut, + mut x: DVectorSliceMut, + direction: DVectorSlice, + ) -> Result> { + let p = direction; + x.axpy(T::one(), &p, T::one()); + function.eval_into(&mut f, &DVectorSlice::from(&x)); + Ok(T::one()) + } +} + +/// Standard backtracking line search using the Armijo condition. +/// +/// See Jorge & Nocedal (2006), Numerical Optimization, Chapter 3.1. +/// #[derive(Clone, Debug)] +pub struct BacktrackingLineSearch; + +impl LineSearch for BacktrackingLineSearch +where + T: RealField, + F: VectorFunction, +{ + #[replace_float_literals(T::from_f64(literal).unwrap())] + fn step( + &mut self, + function: &mut F, + mut f: DVectorSliceMut, + mut x: DVectorSliceMut, + direction: DVectorSlice, + ) -> Result> { + // We seek to solve + // F(x) = 0 + // by minimizing + // g(x) = (1/2) || F(x) ||^2 + // We have that + // grad g = (grad F) * F, + // and the sufficient decrease condition becomes + // g(x_k + alpha * p_k) <= g(x_k) + c * alpha * (grad g)^T * p_k + // ~= g(x_k) - c * alpha * g(x_k) + // = (1 - c * alpha) * g(x_k) + // where p_k is the step direction, c is a parameter in (0, 1) + // and we have assumed that + // grad F^T p_k ~= - F(x_k) + // (which it would satisfy if p_k is the exact solution of the Newton step equation). + + // TODO: Is this an OK parameter? Should anyway make it configurable + let c = 1e-4; + let alpha_min = 1e-6; + + let p = direction; + let g_initial = 0.5 * f.magnitude_squared(); + + // Start out with some alphas that don't decrease too quickly, then + // start decreasing them much faster if the first few iterations don't let us + // take a step. + let initial_alphas = [0.0, 1.0, 0.75, 0.5]; + let mut alpha_iter = initial_alphas + .iter() + .copied() + .chain(iterate(0.25, |alpha_i| 0.25 * *alpha_i)); + + let mut alpha_prev = alpha_iter.next().unwrap(); + let mut alpha = alpha_iter.next().unwrap(); + + loop { + let delta_alpha = alpha - alpha_prev; + + // We have that x^{k + 1} = x^0 + alpha^k * p, + // where x^{k+1} is the value of x after taking the step based on the current alpha + // parameter. It is straightforward to show that this implies that + // x^{k + 1} = x^k + (alpha^k - alpha^{k - 1}) * p, + // which is far more amenable to computation + x.axpy(delta_alpha, &p, T::one()); + function.eval_into(&mut f, &DVectorSlice::from(&x)); + + let g = 0.5 * f.magnitude_squared(); + if g <= (1.0 - c * alpha) * g_initial { + break; + } else if alpha < alpha_min { + return Err(Box::from(format!( + "Failed to produce valid step direction.\ + Alpha {} is smaller than minimum allowed alpha {}.", + alpha, alpha_min + ))); + } else { + alpha_prev = alpha; + alpha = alpha_iter.next().unwrap(); + } + } + + Ok(alpha) + } +} diff --git a/hamilton2/tests/unit.rs b/hamilton2/tests/unit.rs new file mode 100644 index 0000000..c10007d --- /dev/null +++ b/hamilton2/tests/unit.rs @@ -0,0 +1,4 @@ +mod unit_tests; + +#[macro_use] +pub mod utils; diff --git a/hamilton2/tests/unit_tests/calculus.rs b/hamilton2/tests/unit_tests/calculus.rs new file mode 100644 index 0000000..9d731c6 --- /dev/null +++ b/hamilton2/tests/unit_tests/calculus.rs @@ -0,0 +1,39 @@ +use hamilton2::calculus::*; +use nalgebra::{DMatrix, DVector, DVectorSlice, DVectorSliceMut}; + +#[test] +fn approximate_jacobian_simple_function() { + struct SimpleTwoDimensionalPolynomial; + + // TODO: Rewrite with VectorFunctionBuilder + + impl VectorFunction for SimpleTwoDimensionalPolynomial { + fn dimension(&self) -> usize { + 2 + } + + fn eval_into(&mut self, f: &mut DVectorSliceMut, x: &DVectorSlice) { + assert_eq!(x.len(), 2); + assert_eq!(f.len(), x.len()); + let x1 = x[0]; + let x2 = x[1]; + f[0] = x1 * x2 + 3.0; + f[1] = x1 * x1 + x2 * x2 + x1 + 5.0; + } + } + + let h = 1e-6; + let x = DVector::from_column_slice(&[3.0, 4.0]); + let j = approximate_jacobian(SimpleTwoDimensionalPolynomial, &x, &h); + + // J = [ x2 x1 ] + // [ 2*x1 + 1 2*x2 ] + + #[rustfmt::skip] + let expected = DMatrix::from_row_slice(2, 2, + &[4.0, 3.0, + 7.0, 8.0]); + + let diff = expected - j; + assert!(diff.norm() < 1e-6); +} diff --git a/hamilton2/tests/unit_tests/integrators.rs b/hamilton2/tests/unit_tests/integrators.rs new file mode 100644 index 0000000..5c1bcb6 --- /dev/null +++ b/hamilton2/tests/unit_tests/integrators.rs @@ -0,0 +1,125 @@ +use hamilton2::dynamic_system::{IntoStateful, StatelessDifferentiableDynamicSystem, StatelessDynamicSystem}; +use hamilton2::integrators::{backward_euler_step, symplectic_euler_step, BackwardEulerSettings}; +use nalgebra::{DVector, DVectorSlice, DVectorSliceMut, Vector3}; +use std::error::Error; + +use crate::assert_approx_matrix_eq; + +/// Define the (mock) dynamic system +/// M dv/dt + f(t, u, v) = 0 +/// du/dt - v = 0, +/// with +/// M = a * I +/// f(t, u, v) = b * t * ones + c * u + d * v +pub struct MockDynamicSystem { + a: f64, + b: f64, + c: f64, + d: f64, +} + +impl StatelessDynamicSystem for MockDynamicSystem { + fn apply_mass_matrix(&self, mut y: DVectorSliceMut, x: DVectorSlice) { + y.axpy(self.a, &x, 1.0); + } + + fn apply_inverse_mass_matrix( + &self, + mut y: DVectorSliceMut, + x: DVectorSlice, + ) -> Result<(), Box> { + y.copy_from(&(x / self.a)); + Ok(()) + } + + fn eval_f(&self, mut f: DVectorSliceMut, t: f64, u: DVectorSlice, v: DVectorSlice) { + let ones = DVector::repeat(u.len(), 1.0); + f.copy_from(&(self.b * t * ones + self.c * u + self.d * v)); + } +} + +impl StatelessDifferentiableDynamicSystem for MockDynamicSystem { + fn apply_jacobian_combination( + &self, + _y: DVectorSliceMut, + _x: DVectorSlice, + _t: f64, + _u: DVectorSlice, + _v: DVectorSlice, + _alpha: Option, + _beta: Option, + _gamma: Option, + ) -> Result<(), Box> { + unimplemented!(); + } + + fn solve_jacobian_combination( + &self, + mut sol: DVectorSliceMut, + rhs: DVectorSlice, + _t: f64, + _u: DVectorSlice, + _v: DVectorSlice, + alpha: Option, + beta: Option, + ) -> Result<(), Box> { + let alpha = alpha.unwrap_or(0.0); + let beta = beta.unwrap_or(0.0); + let g = self.a + alpha * self.c + beta * self.d; + sol.copy_from(&(rhs / g)); + Ok(()) + } +} + +#[test] +fn symplectic_euler_step_mock() { + let (a, b, c, d) = (2.0, 3.0, 4.0, 5.0); + let mut system = MockDynamicSystem { a, b, c, d }.into_stateful(); + let t0 = 2.0; + let dt = 0.5; + + let mut u = Vector3::new(1.0, 2.0, 3.0); + let mut v = Vector3::new(4.0, 5.0, 6.0); + + let mut f = Vector3::zeros(); + let mut dv = Vector3::zeros(); + + // Compute expected values + let v_next_expected = &v + (dt / a) * (b * t0 * Vector3::repeat(1.0) + c * &u + d * &v); + let u_next_expected = &u + dt * &v_next_expected; + + symplectic_euler_step(&mut system, &mut u, &mut v, &mut f, &mut dv, t0, dt).unwrap(); + + assert_approx_matrix_eq!(v, v_next_expected, abstol = 1e-8); + assert_approx_matrix_eq!(u, u_next_expected, abstol = 1e-8); +} + +#[test] +fn backward_euler_step_mock() { + let (a, b, c, d) = (2.0, 3.0, 4.0, 5.0); + let mut system = MockDynamicSystem { a, b, c, d }.into_stateful(); + let t0 = 2.0; + let dt = 0.5; + + let mut u = Vector3::new(1.0, 2.0, 3.0); + let mut v = Vector3::new(4.0, 5.0, 6.0); + + let settings = BackwardEulerSettings { + max_newton_iter: Some(100), + tolerance: 1e-8, + }; + + let (u_next_expected, v_next_expected) = { + let h = a - dt * d - dt * dt * c; + let t = t0 + dt; + let ones = Vector3::repeat(1.0); + let v_next_expected = (a * &v + dt * (b * t * ones + c * &u)) / h; + let u_next_expected = &u + dt * &v_next_expected; + (u_next_expected, v_next_expected) + }; + + backward_euler_step(&mut system, &mut u, &mut v, t0, dt, settings).unwrap(); + + assert_approx_matrix_eq!(v, v_next_expected, abstol = 1e-6); + assert_approx_matrix_eq!(u, u_next_expected, abstol = 1e-6); +} diff --git a/hamilton2/tests/unit_tests/mod.rs b/hamilton2/tests/unit_tests/mod.rs new file mode 100644 index 0000000..c5f4c78 --- /dev/null +++ b/hamilton2/tests/unit_tests/mod.rs @@ -0,0 +1,3 @@ +mod calculus; +mod integrators; +mod newton; diff --git a/hamilton2/tests/unit_tests/newton.rs b/hamilton2/tests/unit_tests/newton.rs new file mode 100644 index 0000000..48dbee9 --- /dev/null +++ b/hamilton2/tests/unit_tests/newton.rs @@ -0,0 +1,58 @@ +use hamilton2::calculus::{DifferentiableVectorFunction, VectorFunction}; +use hamilton2::newton::*; +use nalgebra::{DVector, DVectorSlice, DVectorSliceMut, Matrix3, Vector3}; +use numeric_literals::replace_numeric_literals; +use std::error::Error; + +struct MockLinearVectorFunction; + +impl VectorFunction for MockLinearVectorFunction { + fn dimension(&self) -> usize { + 3 + } + + #[replace_numeric_literals(f64::from(literal))] + fn eval_into(&mut self, f: &mut DVectorSliceMut, x: &DVectorSlice) { + let a = Matrix3::new(5, 1, 2, 1, 4, 2, 2, 2, 4); + let b = Vector3::new(1, 2, 3); + let r = a * x - b; + f.copy_from(&r); + } +} + +impl DifferentiableVectorFunction for MockLinearVectorFunction { + #[replace_numeric_literals(f64::from(literal))] + fn solve_jacobian_system( + &mut self, + sol: &mut DVectorSliceMut, + _x: &DVectorSlice, + rhs: &DVectorSlice, + ) -> Result<(), Box> { + let a = Matrix3::new(5, 1, 2, 1, 4, 2, 2, 2, 4); + let a_inv = a.try_inverse().unwrap(); + sol.copy_from(&(a_inv * rhs)); + Ok(()) + } +} + +#[test] +fn newton_converges_in_single_iteration_for_linear_system() { + // TODO: Use VectorFunctionBuilder or a mock from mockiato + + let expected_solution = Vector3::new(-0.125, 0.16666667, 0.72916667); + + let settings = NewtonSettings { + max_iterations: Some(2), + tolerance: Vector3::new(1.0, 2.0, 3.0).norm() * 1e-6, + }; + + let mut f = DVector::zeros(3); + let mut x = DVector::zeros(3); + let mut dx = DVector::zeros(3); + + let iterations = + newton(MockLinearVectorFunction, &mut x, &mut f, &mut dx, settings).expect("Newton iterations must succeed"); + let diff = x - expected_solution; + assert!(diff.norm() < 1e-6); + assert_eq!(iterations, 1); +} diff --git a/hamilton2/tests/utils/mod.rs b/hamilton2/tests/utils/mod.rs new file mode 100644 index 0000000..8b62433 --- /dev/null +++ b/hamilton2/tests/utils/mod.rs @@ -0,0 +1,31 @@ +/// Poor man's approx assertion for matrices +#[macro_export] +macro_rules! assert_approx_matrix_eq { + ($x:expr, $y:expr, abstol = $tol:expr) => {{ + let diff = $x - $y; + + let max_absdiff = diff.abs().max(); + let approx_eq = max_absdiff <= $tol; + + if !approx_eq { + println!("abstol: {}", $tol); + println!("left: {}", $x); + println!("right: {}", $y); + println!("diff: {:e}", diff); + } + assert!(approx_eq); + }}; +} + +#[macro_export] +macro_rules! assert_panics { + ($e:expr) => {{ + use std::panic::catch_unwind; + use std::stringify; + let expr_string = stringify!($e); + let result = catch_unwind(|| $e); + if result.is_ok() { + panic!("assert_panics!({}) failed.", expr_string); + } + }}; +} diff --git a/intel-mkl-src-patched/Cargo.toml b/intel-mkl-src-patched/Cargo.toml new file mode 100644 index 0000000..0d073c3 --- /dev/null +++ b/intel-mkl-src-patched/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "intel-mkl-src" +version = "0.5.0" +authors = ["Andreas Longva "] +edition = "2018" +publish = false + +# This is a dummy package that circumvents the intel-mkl-src package, which attempts to download MKL as part of the +# build project. We replace it in-tree by this dummy package, which instead uses mkl-sys to link +# to MKL. + +[features] +use-shared = [] + +[dependencies] +# Note: This should match the same version as mkl-corrode is using +[dependencies.mkl-sys] +git = "https://github.com/Andlon/mkl-sys" +rev = "e144301e42bf984e28b53026d366cfacbefa6d0f" \ No newline at end of file diff --git a/intel-mkl-src-patched/src/lib.rs b/intel-mkl-src-patched/src/lib.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/intel-mkl-src-patched/src/lib.rs @@ -0,0 +1 @@ + diff --git a/lp-bfp/Cargo.toml b/lp-bfp/Cargo.toml new file mode 100644 index 0000000..bec0e25 --- /dev/null +++ b/lp-bfp/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "lp-bfp" +version = "0.1.0" +authors = ["Andreas Longva "] +edition = "2018" +links = "ortools" +publish = false + +[dependencies] +libc = "0.2.66" + +[dev-dependencies] +nalgebra = "0.21" + +[build-dependencies] +cmake = "0.1" diff --git a/lp-bfp/README.md b/lp-bfp/README.md new file mode 100644 index 0000000..de50c37 --- /dev/null +++ b/lp-bfp/README.md @@ -0,0 +1,13 @@ +lp-bfp +====== + +A tiny Rust library written as a wrapper around a small C++ project that relies on an external LP solver to find a basic feasible point of a Linear Program. + +To use, you need: + +- To install Google OR-Tools. Get it here: https://developers.google.com/optimization. +- To set the environment variable `ORTOOLS_ROOT` to the root directory of your OR-Tools installation. +The root directory is the folder containing `lib`, `bin` etc. +- To make sure that the `lib` directory in `ORTOOLS_ROOT` is available in your library search path. +For Linux platforms, this can be accomplished by e.g. including `$ORTOOLS_ROOT/lib` in the `$LD_LIBRARY_PATH` environment +variable. For Windows, this can be accomplished by including the lib directory in `PATH` (TODO: Is this correct?). diff --git a/lp-bfp/build.rs b/lp-bfp/build.rs new file mode 100644 index 0000000..00c3671 --- /dev/null +++ b/lp-bfp/build.rs @@ -0,0 +1,27 @@ +use cmake; +use std::env::vars; + +fn main() { + let cpp = cmake::build("cpp"); + + let or_tools_root = vars() + .find(|(var, _)| var == "ORTOOLS_ROOT") + .map(|(_, value)| value) + .expect("Could not find ORTOOLS_ROOT"); + + let or_tools_lib_path = format!("{}/lib", or_tools_root); + + // Link static shim library + println!("cargo:rustc-link-search=native={}/lib", cpp.display()); + println!("cargo:rustc-link-lib=static=lp-bfp"); + + // Link OR-tools + println!("cargo:rustc-link-search=native={}", &or_tools_lib_path); + println!("cargo:rustc-link-lib=dylib=ortools"); + + // TODO: Come up with something more general? + if cfg!(target_env = "gnu") { + // GCC-specific, link to C++ stdlib + println!("cargo:rustc-link-lib=dylib=stdc++"); + } +} diff --git a/lp-bfp/cpp/CMakeLists.txt b/lp-bfp/cpp/CMakeLists.txt new file mode 100644 index 0000000..ef4353d --- /dev/null +++ b/lp-bfp/cpp/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.10) +project(cpp) + +set(CMAKE_CXX_STANDARD 14) +set(WINDOWS_EXPORT_ALL_SYMBOLS true) + +SET(ORTOOLS_ROOT $ENV{ORTOOLS_ROOT}) +set(ORTOOLS_INCLUDE ${ORTOOLS_ROOT}/include) +set(ORTOOLS_LIB ${ORTOOLS_ROOT}/lib) + +add_library(lp-bfp STATIC lp-bfp.cpp lp-bfp.h) +target_include_directories(lp-bfp SYSTEM PUBLIC ${ORTOOLS_INCLUDE}) +target_link_libraries(lp-bfp libortools) + +# Ideally we'd use target_link_directories, but it is only supported by fairly recent versions of CMake +link_directories(${ORTOOLS_LIB}) + +IF (WIN32) + # OR-Tools wants to define min/max functions but also includes windows.h which defines min/max macros itself + # Therefore add a flag that disables the min/max macros from windows.h + target_compile_definitions(lp-bfp PUBLIC NOMINMAX) +ENDIF() + +install(TARGETS lp-bfp + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin) \ No newline at end of file diff --git a/lp-bfp/cpp/lp-bfp.cpp b/lp-bfp/cpp/lp-bfp.cpp new file mode 100644 index 0000000..c0a9ae0 --- /dev/null +++ b/lp-bfp/cpp/lp-bfp.cpp @@ -0,0 +1,84 @@ +#include "lp-bfp.h" + +#include +#include +#include + +int lp_bfp_solve_lp(double * x, + const double * c, + const double * A, + const double * b, + const double * lb, + const double * ub, + int num_constraints, + int num_variables, + bool verbose) +{ + using operations_research::glop::LinearProgram; + using operations_research::glop::ColIndex; + using operations_research::glop::ProblemStatus; + using operations_research::glop::LPSolver; + using operations_research::glop::GlopParameters; + using operations_research::glop::GlopParameters_SolverBehavior; + using std::cout; + + LinearProgram lp; + + std::vector variables; + for (int i = 0; i < num_variables; ++i) + { + const auto variable = lp.CreateNewVariable(); + lp.SetVariableBounds(variable, lb[i], ub[i]); + lp.SetObjectiveCoefficient(variable, c[i]); + variables.push_back(variable); + } + + for (int i = 0; i < num_constraints; ++i) + { + const auto constraint_row = lp.CreateNewConstraint(); + lp.SetConstraintBounds(constraint_row, b[i], b[i]); + + for (int j = 0; j < num_variables; ++j) + { + const auto linear_matrix_index = num_variables * i + j; + const auto a_ij = A[linear_matrix_index]; + // GLOP uses sparse matrices under the hood, and it does not like explicit zeros. + if (a_ij != 0.0) { + lp.SetCoefficient(constraint_row, variables[j], a_ij); + } + } + } + + if (verbose) { + cout << "Constructed LP with " << num_variables << " variables and " << num_constraints << " constraints." + << std::endl; + } + + auto params = GlopParameters(); + // Try to make GLOP change the problem as little as possible, + // so that we hopefully get a more precise solution + params.set_primal_feasibility_tolerance(1e-14); + params.set_drop_tolerance(0.0); + params.set_solve_dual_problem(GlopParameters_SolverBehavior::GlopParameters_SolverBehavior_NEVER_DO); + params.set_use_dual_simplex(false); + + LPSolver glop_solver; + glop_solver.SetParameters(params); + const auto status = glop_solver.Solve(lp); + + if (verbose) { + std::cout << lp.GetPrettyProblemStats() << std::endl; + std::cout << "Status: " << status << std::endl; + } + + if (status == ProblemStatus::OPTIMAL) { + for (int i = 0; i < num_variables; ++i) { + x[i] = glop_solver.variable_values().at(variables.at(i)); + } + + return 0; + } else { + // TODO: Handle error conditions + return 1; + } +} \ No newline at end of file diff --git a/lp-bfp/cpp/lp-bfp.h b/lp-bfp/cpp/lp-bfp.h new file mode 100644 index 0000000..1596828 --- /dev/null +++ b/lp-bfp/cpp/lp-bfp.h @@ -0,0 +1,35 @@ +#ifndef LP_BFP_H +#define LP_BFP_H + +extern "C" { + /// Attempts to solve the given Linear Program, storing the result in `x`. + /// + /// The LP is given by + /// min c^T x + /// s.t. Ax = b + /// lb <= x <= ub + /// Let A be an m x n matrix. Then the LP has m equality constraints and + /// n (possibly bounded) variables. + /// + /// \param x Output array of length n. + /// \param c Array of length n. + /// \param A Row-major m x n matrix stored as array of length mxn. + /// \param b Array of length m. + /// \param lb Array of length n. + /// \param ub Array of length n. + /// \param num_constraints m. + /// \param num_variables n. + /// \param verbose Whether or not to print debug output to stdout. + /// \return An error code. 0 denotes success. + int lp_bfp_solve_lp(double * x, + const double * c, + const double * A, + const double * b, + const double * lb, + const double * ub, + int num_constraints, + int num_variables, + bool verbose); +}; + +#endif // LP_BFP_H diff --git a/lp-bfp/src/lib.rs b/lp-bfp/src/lib.rs new file mode 100644 index 0000000..ecff31e --- /dev/null +++ b/lp-bfp/src/lib.rs @@ -0,0 +1,130 @@ +use libc::{c_double, c_int}; +use std::convert::TryFrom; +use std::error::Error; +use std::fmt; +use std::fmt::{Display, Formatter}; + +extern "C" { + fn lp_bfp_solve_lp( + x: *mut c_double, + c: *const c_double, + a: *const c_double, + b: *const c_double, + lb: *const c_double, + ub: *const c_double, + num_constraints: c_int, + num_variables: c_int, + verbose: bool, + ) -> c_int; +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Verbosity { + NoVerbose, + Verbose, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct BfpError { + // Private construction outside of library + private: (), +} + +impl Display for BfpError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "Unspecified error during solve.") + } +} + +impl Error for BfpError {} + +pub fn solve_lp( + c: &[f64], + a: &[f64], + b: &[f64], + lb: &[f64], + ub: &[f64], + verbosity: Verbosity, +) -> Result, BfpError> { + assert_eq!(lb.len(), ub.len(), "Lower and upper bounds must have same length."); + let num_constraints = b.len(); + let num_variables = lb.len(); + assert_eq!( + a.len(), + num_constraints * num_variables, + "Number of elements in a must be consistent with number of \ + variables and constraints." + ); + assert_eq!( + c.len(), + num_variables, + "Length of c must be equal to number of variables." + ); + + let verbose = verbosity == Verbosity::Verbose; + let mut solution = vec![0.0; num_variables]; + + // TODO: Make error? + let num_constraints = + c_int::try_from(num_constraints).expect("Number of constraints is too large to fit in `int`."); + let num_variables = c_int::try_from(num_variables).expect("Number of variables is too large to fit in `int`."); + + let error_code = unsafe { + lp_bfp_solve_lp( + solution.as_mut_ptr(), + c.as_ptr(), + a.as_ptr(), + b.as_ptr(), + lb.as_ptr(), + ub.as_ptr(), + num_constraints, + num_variables, + verbose, + ) + }; + + if error_code == 0 { + Ok(solution) + } else { + Err(BfpError { private: () }) + } +} + +#[cfg(test)] +mod tests { + use super::{solve_lp, Verbosity}; + use nalgebra::{DMatrix, DVector}; + + #[test] + fn solve_basic_lp() { + // Problem is constructed by constructing b such that A * x0 = b + let x0 = vec![0.0, 3.0, 7.0, 0.0, 1.0, 0.0]; + let b = vec![28.0, -15.0, 58.0]; + let a = vec![ + 1.0, -1.0, 4.0, 2.0, 3.0, 3.0, -5.0, 2.0, -3.0, 2.0, 0.0, 4.0, 3.0, 1.0, 7.0, -3.0, 6.0, 0.0, + ]; + let lb = vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; + let ub = vec![2.0, 4.0, 8.0, 9.0, 2.0, 3.0]; + let c = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; + + let x = solve_lp(&c, &a, &b, &lb, &ub, Verbosity::NoVerbose).unwrap(); + + // Check bounds + for i in 0..x.len() { + assert!(lb[i] <= x[i] && x[i] <= ub[i]); + } + + // Check equality constraints + let a = DMatrix::from_row_slice(3, 6, &a); + let x = DVector::from_column_slice(&x); + let b = DVector::from_column_slice(&b); + let c = DVector::from_column_slice(&c); + let x0 = DVector::from_column_slice(&x0); + let r = a * &x - b; + + let objective_val = c.dot(&x); + assert!(r.norm() < 1e-12); + // TODO: Have a better, more "complete" test? + assert!(objective_val <= c.dot(&x0) + 1e-6); + } +} diff --git a/nested-vec/Cargo.toml b/nested-vec/Cargo.toml new file mode 100644 index 0000000..9bd4963 --- /dev/null +++ b/nested-vec/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "nested-vec" +version = "0.1.0" +authors = ["Andreas Longva "] +edition = "2018" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# TODO: Make optional feature +serde = { version="1.0", features = [ "derive" ] } \ No newline at end of file diff --git a/nested-vec/src/lib.rs b/nested-vec/src/lib.rs new file mode 100644 index 0000000..0b90e7e --- /dev/null +++ b/nested-vec/src/lib.rs @@ -0,0 +1,177 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::fmt::Debug; +use std::ops::Range; + +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct NestedVec { + data: Vec, + offsets_begin: Vec, + offsets_end: Vec, +} + +impl Debug for NestedVec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list().entries(self.iter()).finish() + } +} + +impl Default for NestedVec { + fn default() -> Self { + Self::new() + } +} + +impl NestedVec { + pub fn new() -> Self { + Self { + data: Vec::new(), + offsets_begin: Vec::new(), + offsets_end: Vec::new(), + } + } + + /// Return a data structure that can be used for appending single elements to the same array. + /// When the returned data structure is dropped, the result is equivalent to + /// adding the array at once with `CompactArrayStorage::push`. + /// + /// TODO: Need better name + pub fn begin_array<'a>(&'a mut self) -> ArrayAppender<'a, T> { + let initial_count = self.data.len(); + ArrayAppender { + initial_count, + data: &mut self.data, + offsets_begin: &mut self.offsets_begin, + offsets_end: &mut self.offsets_end, + } + } + + pub fn iter<'a>(&'a self) -> impl 'a + Iterator { + (0..self.len()).map(move |i| self.get(i).unwrap()) + } + + pub fn len(&self) -> usize { + self.offsets_begin.len() + } + + /// Returns an iterator over all elements inside all arrays. + pub fn iter_array_elements<'a>(&'a self) -> impl 'a + Iterator { + self.iter().flatten() + } + + pub fn total_num_elements(&self) -> usize { + self.offsets_begin + .iter() + .zip(&self.offsets_end) + .map(|(begin, end)| end - begin) + .sum() + } + + pub fn get(&self, index: usize) -> Option<&[T]> { + let range = self.get_index_range(index)?; + self.data.get(range) + } + + pub fn get_mut(&mut self, index: usize) -> Option<&mut [T]> { + let range = self.get_index_range(index)?; + self.data.get_mut(range) + } + + fn get_index_range(&self, index: usize) -> Option> { + let begin = *self.offsets_begin.get(index)?; + let end = *self.offsets_end.get(index)?; + Some(begin..end) + } + + pub fn first(&self) -> Option<&[T]> { + self.get(0) + } + + pub fn first_mut(&mut self) -> Option<&mut [T]> { + self.get_mut(0) + } + + fn get_last_range(&self) -> Option> { + let begin = *self.offsets_begin.last()?; + let end = *self.offsets_end.last()?; + Some(begin..end) + } + + pub fn last(&self) -> Option<&[T]> { + let range = self.get_last_range()?; + self.data.get(range) + } + + pub fn last_mut(&mut self) -> Option<&mut [T]> { + let range = self.get_last_range()?; + self.data.get_mut(range) + } + + pub fn clear(&mut self) { + self.offsets_end.clear(); + self.offsets_begin.clear(); + self.data.clear(); + } +} + +#[derive(Debug)] +pub struct ArrayAppender<'a, T> { + data: &'a mut Vec, + offsets_begin: &'a mut Vec, + offsets_end: &'a mut Vec, + initial_count: usize, +} + +impl<'a, T> ArrayAppender<'a, T> { + pub fn push_single(&mut self, element: T) -> &mut Self { + self.data.push(element); + self + } + + pub fn count(&self) -> usize { + self.data.len() - self.initial_count + } +} + +impl<'a, T> Drop for ArrayAppender<'a, T> { + fn drop(&mut self) { + self.offsets_begin.push(self.initial_count); + self.offsets_end.push(self.data.len()); + } +} + +impl NestedVec { + pub fn push(&mut self, array: &[T]) { + self.offsets_begin.push(self.data.len()); + self.data.extend_from_slice(array); + self.offsets_end.push(self.data.len()); + } +} + +impl<'a, T: Clone> From<&'a Vec>> for NestedVec { + fn from(nested_vec: &'a Vec>) -> Self { + let mut result = Self::new(); + for vec in nested_vec { + result.push(vec); + } + result + } +} + +impl From>> for NestedVec { + fn from(vec_vec: Vec>) -> Self { + Self::from(&vec_vec) + } +} + +impl<'a, T: Clone> From<&'a NestedVec> for Vec> { + fn from(nested: &NestedVec) -> Self { + nested.iter().map(|slice| slice.to_vec()).collect() + } +} + +impl<'a, T: Clone> From> for Vec> { + fn from(nested: NestedVec) -> Self { + Self::from(&nested) + } +} diff --git a/nested-vec/tests/test.rs b/nested-vec/tests/test.rs new file mode 100644 index 0000000..c5a0fd1 --- /dev/null +++ b/nested-vec/tests/test.rs @@ -0,0 +1,50 @@ +use nested_vec::NestedVec; + +#[test] +fn begin_array() { + let mut storage = NestedVec::::new(); + + { + // Add empty array + storage.begin_array(); + } + + assert_eq!(storage.len(), 1); + assert_eq!(storage.get(0).unwrap(), []); + assert!(storage.get(1).is_none()); + assert_eq!( + Vec::::new(), + storage.iter().flatten().cloned().collect::>() + ); + + { + storage.begin_array().push_single(5).push_single(9); + } + + assert_eq!(storage.len(), 2); + assert_eq!(storage.get(0).unwrap(), []); + assert_eq!(storage.get(1).unwrap(), [5, 9]); + assert!(storage.get(2).is_none()); + + { + // Add empty array + storage.begin_array(); + } + + assert_eq!(storage.len(), 3); + assert_eq!(storage.get(0).unwrap(), []); + assert_eq!(storage.get(1).unwrap(), [5, 9]); + assert_eq!(storage.get(2).unwrap(), []); + assert!(storage.get(3).is_none()); + + { + storage.begin_array().push_single(3); + } + + assert_eq!(storage.len(), 4); + assert_eq!(storage.get(0).unwrap(), []); + assert_eq!(storage.get(1).unwrap(), [5, 9]); + assert_eq!(storage.get(2).unwrap(), []); + assert_eq!(storage.get(3).unwrap(), [3]); + assert!(storage.get(4).is_none()); +} diff --git a/notebooks/condition_number_experiment/condition_number_experiment.ipynb b/notebooks/condition_number_experiment/condition_number_experiment.ipynb new file mode 100644 index 0000000..9f6f9cc --- /dev/null +++ b/notebooks/condition_number_experiment/condition_number_experiment.ipynb @@ -0,0 +1,197 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib import rc\n", + "from matplotlib.ticker import MultipleLocator\n", + "\n", + "FOR_PRINT = True\n", + "\n", + "if FOR_PRINT:\n", + " LINE_WIDTH = 1\n", + " MARKER_SIZE = 3\n", + " FONT_SIZE = 8\n", + "\n", + " AXES_WIDTH = 0.65 * LINE_WIDTH\n", + "\n", + " plt.rcParams['grid.linewidth']=AXES_WIDTH\n", + " plt.rcParams['axes.linewidth']=AXES_WIDTH\n", + " plt.rcParams['axes.labelpad']=3.0\n", + "\n", + " plt.rcParams['xtick.major.pad']=0\n", + " plt.rcParams['xtick.major.size']=2.0\n", + " plt.rcParams['xtick.major.width']=AXES_WIDTH\n", + " plt.rcParams['xtick.minor.size']=1.0\n", + " plt.rcParams['xtick.minor.width']=0.75 * AXES_WIDTH\n", + "\n", + " plt.rcParams['ytick.major.pad']=-1.5\n", + " plt.rcParams['ytick.major.size']=2.0\n", + " plt.rcParams['ytick.major.width']=AXES_WIDTH\n", + " plt.rcParams['ytick.minor.size']=1.0\n", + " plt.rcParams['ytick.minor.width']=0.75 * AXES_WIDTH\n", + "else:\n", + " LINE_WIDTH = 6\n", + " MARKER_SIZE = 14\n", + " FONT_SIZE = 45\n", + "\n", + "%matplotlib inline\n", + "#plt.rcParams['figure.figsize'] = [15, 15]\n", + "plt.rcParams['lines.linewidth'] = LINE_WIDTH\n", + "plt.rcParams['lines.markeredgewidth'] = 0.75 * LINE_WIDTH\n", + "plt.rcParams['lines.markersize'] = MARKER_SIZE\n", + "plt.rcParams['font.size'] = FONT_SIZE\n", + "rc('text', usetex=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = list()\n", + "with open(\"condition_numbers.json\") as results_json_file:\n", + " data = json.load(results_json_file)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#elements = [\"Tet4\", \"Tet10\", \"Hex8\", \"Hex20\"]\n", + "elements = [\"Tet4\", \"Tet10\", \"Hex8\"]\n", + "stabilization_factors = list(set([obj['stabilization_factor'] for obj in data]))\n", + "preconditioners = list(set([obj['preconditioner_name'] for obj in data]))\n", + "legend_loc = ['upper right', 'upper left']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "def get_epsilon_index(obj):\n", + " return obj['epsilon_index']\n", + "\n", + "def should_keep_preconditioner(element, preconditioner):\n", + " if element == 'Hex8':\n", + " return preconditioner == 'Diagonal'\n", + " #return (element in ['Tet4', 'Hex8'] and preconditioner == 'Diagonal') \\\n", + " #or (element in ['Tet10', 'Hex20'] and preconditioner == 'Schwarz')\n", + " else:\n", + " return True\n", + " \n", + "def get_element_name(element):\n", + " if element == 'Tet4':\n", + " return 'Linear Tet'\n", + " elif element == 'Tet10':\n", + " return 'Quadratic Tet'\n", + " elif element == 'Hex8':\n", + " return 'Trilinear Hex'\n", + " return element\n", + "\n", + "FIG_WIDTH = 3.6 if FOR_PRINT else 20\n", + "FIG_HEIGHT = 2.0 if FOR_PRINT else 10\n", + "\n", + "filenames = {\n", + " 0.0: \"condition_number_no_stabilization.pgf\",\n", + " 1e-06: \"condition_number_stabilized.pgf\"\n", + "}\n", + "\n", + "for (loc, stab_factor) in zip(legend_loc, stabilization_factors):\n", + " fig = plt.figure(figsize=(FIG_WIDTH, FIG_HEIGHT))\n", + " \n", + " for element in elements:\n", + " for preconditioner in preconditioners:\n", + " if should_keep_preconditioner(element, preconditioner):\n", + " sorted_objects = [obj for obj in data if obj['element_name'] == element\n", + " and obj['stabilization_factor'] == stab_factor\n", + " and obj['preconditioner_name'] == preconditioner]\n", + " sorted_objects.sort(key=get_epsilon_index)\n", + " eps_indices = [obj['epsilon_index'] for obj in sorted_objects]\n", + " volume_fraction = [2**(-r) for r in eps_indices]\n", + " mass_condition = [obj['mass_condition']['condition_number'] for obj in sorted_objects]\n", + " stiffness_condition = [obj['stiffness_condition']['condition_number'] for obj in sorted_objects]\n", + " system_condition = [obj['system_condition']['condition_number'] for obj in sorted_objects]\n", + "\n", + " preconditioner_name = preconditioner if preconditioner else \"Unchanged\"\n", + " #label_suffix = \" {} ({}) (stab = {:1.1e})\".format(element, preconditioner_name, stab_factor)\n", + " label_suffix = \" {} ({})\".format(get_element_name(element), preconditioner_name, stab_factor)\n", + "# plt.plot(volume_fraction, mass_condition, label=r'$\\kappa(M)$' + label_suffix)\n", + "# plt.plot(volume_fraction, stiffness_condition, label='$\\kappa(K)$' + label_suffix)\n", + " plt.plot(volume_fraction, system_condition, marker='o', label=label_suffix)\n", + " plt.legend(prop={'size': 0.75 * FONT_SIZE}, loc =loc)\n", + "\n", + " plt.grid()\n", + " plt.xscale('log')\n", + " plt.yscale('log')\n", + " plt.xlabel('Volume fraction', fontsize=FONT_SIZE)\n", + " #plt.ylabel('$\\kappa(P(M + (\\Delta t)^2 K))$', fontsize=45)\n", + " plt.ylabel('$\\kappa(SA)$', fontsize=FONT_SIZE, labelpad=plt.rcParams['axes.labelpad']/2)\n", + " plt.tick_params(axis='both', which='major', labelsize=FONT_SIZE)\n", + " if stab_factor == 0.0:\n", + " #plt.title(\"Condition number, no stabilization\", fontsize=45)\n", + " pass\n", + " else:\n", + " #plt.title(\"Condition number, stabilization factor ${}$\".format(stab_factor), fontsize=45)\n", + " pass\n", + " plt.tight_layout()\n", + " #plt.savefig('condition_number_stab_{}.pdf'.format(stab_factor), format='pdf', bbox_inches='tight')\n", + " filename = 'condition_number_stab_{}.pgf'.format(stab_factor)\n", + " if stab_factor in filenames:\n", + " filename = filenames[stab_factor]\n", + " plt.savefig(filename, format='pgf', bbox_inches='tight')\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/condition_number_experiment/condition_numbers.json b/notebooks/condition_number_experiment/condition_numbers.json new file mode 100644 index 0000000..c38f7d0 --- /dev/null +++ b/notebooks/condition_number_experiment/condition_numbers.json @@ -0,0 +1 @@ +[{"element_name":"Tet4","epsilon_index":0,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-88222.15650190717,"max_eigval":29093543.967164695,"condition_number":3275.7115324732413},"mass_condition":{"min_eigval":1.0331082388056638,"max_eigval":29.812566391665033,"condition_number":28.85715675458186},"system_condition":{"min_eigval":-15.77644747019487,"max_eigval":8093.15050506001,"condition_number":263645.9550482405}},{"element_name":"Tet4","epsilon_index":0,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.03561327953985008,"max_eigval":4.5159897288327,"condition_number":1361.0089214612253},"mass_condition":{"min_eigval":0.5097354288165642,"max_eigval":2.450146526512886,"condition_number":4.806702434243799},"system_condition":{"min_eigval":-0.023198814543804545,"max_eigval":4.49048692970421,"condition_number":112996.76235537097}},{"element_name":"Tet4","epsilon_index":0,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.03561327953985008,"max_eigval":4.5159897288327,"condition_number":1361.0089214612253},"mass_condition":{"min_eigval":0.5097354288165642,"max_eigval":2.450146526512886,"condition_number":4.806702434243799},"system_condition":{"min_eigval":-0.023198814543804545,"max_eigval":4.49048692970421,"condition_number":112996.76235537097}},{"element_name":"Tet10","epsilon_index":0,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-114037.0331038261,"max_eigval":23465288.0043721,"condition_number":17568.495059848075},"mass_condition":{"min_eigval":0.13866695954001637,"max_eigval":9.282037368378827,"condition_number":66.93762810671727},"system_condition":{"min_eigval":-30.46538527627954,"max_eigval":6521.079366106795,"condition_number":18427.568793219703}},{"element_name":"Tet10","epsilon_index":0,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.0691410400685376,"max_eigval":5.493234886945303,"condition_number":8165.486719514965},"mass_condition":{"min_eigval":0.2531949656547453,"max_eigval":4.29939707251273,"condition_number":16.980578825470623},"system_condition":{"min_eigval":-0.06659730000439779,"max_eigval":5.484991434911079,"condition_number":9697.106653188433}},{"element_name":"Tet10","epsilon_index":0,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.0691410400685376,"max_eigval":5.493234886945303,"condition_number":8165.486719514965},"mass_condition":{"min_eigval":0.2531949656547453,"max_eigval":4.29939707251273,"condition_number":16.980578825470623},"system_condition":{"min_eigval":-0.06659730000439779,"max_eigval":5.484991434911079,"condition_number":9697.106653188433}},{"element_name":"Hex8","epsilon_index":0,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-118455.42261236532,"max_eigval":11737827.292140154,"condition_number":3064.9025828703666},"mass_condition":{"min_eigval":0.6863319463202785,"max_eigval":29.14689087494684,"condition_number":42.467629594128454},"system_condition":{"min_eigval":-23.89882595806931,"max_eigval":3271.8073593768218,"condition_number":2014.928251098774}},{"element_name":"Hex8","epsilon_index":0,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.10605920318004963,"max_eigval":5.462814834194444,"condition_number":1471.6322454135307},"mass_condition":{"min_eigval":0.21947014912832452,"max_eigval":3.2524053226811103,"condition_number":14.819351677659927},"system_condition":{"min_eigval":-0.07827462456913495,"max_eigval":5.412483101178373,"condition_number":913.826682591498}},{"element_name":"Hex8","epsilon_index":0,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.10605920318004963,"max_eigval":5.462814834194444,"condition_number":1471.6322454135307},"mass_condition":{"min_eigval":0.21947014912832452,"max_eigval":3.2524053226811103,"condition_number":14.819351677659927},"system_condition":{"min_eigval":-0.07827462456913495,"max_eigval":5.412483101178373,"condition_number":913.826682591498}},{"element_name":"Hex20","epsilon_index":0,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-176043.6490424629,"max_eigval":38148756.93690972,"condition_number":57216.981420195785},"mass_condition":{"min_eigval":0.008234266958511476,"max_eigval":77.74316744561354,"condition_number":9441.419356127764},"system_condition":{"min_eigval":-47.451627334790125,"max_eigval":10654.922255530299,"condition_number":31128.981264522714}},{"element_name":"Hex20","epsilon_index":0,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.1179405082619155,"max_eigval":7.945289110081971,"condition_number":22677.7500107819},"mass_condition":{"min_eigval":0.0016404891461063342,"max_eigval":11.249701931389113,"condition_number":6857.529023029527},"system_condition":{"min_eigval":-0.11376402635692061,"max_eigval":7.95111575372152,"condition_number":13272.899189456335}},{"element_name":"Hex20","epsilon_index":0,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.1179405082619155,"max_eigval":7.945289110081971,"condition_number":22677.7500107819},"mass_condition":{"min_eigval":0.0016404891461063342,"max_eigval":11.249701931389113,"condition_number":6857.529023029527},"system_condition":{"min_eigval":-0.11376402635692061,"max_eigval":7.95111575372152,"condition_number":13272.899189456335}},{"element_name":"Tet4","epsilon_index":0,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-88222.15650190148,"max_eigval":29093543.96716468,"condition_number":3275.711532470895},"mass_condition":{"min_eigval":1.0331082388056638,"max_eigval":29.812566391665033,"condition_number":28.85715675458186},"system_condition":{"min_eigval":-15.77644747019487,"max_eigval":8093.15050506001,"condition_number":263645.9550482405}},{"element_name":"Tet4","epsilon_index":0,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.03561327953985008,"max_eigval":4.5159897288327,"condition_number":1361.0089214612253},"mass_condition":{"min_eigval":0.5097354288165642,"max_eigval":2.450146526512886,"condition_number":4.806702434243799},"system_condition":{"min_eigval":-0.023198814543804545,"max_eigval":4.49048692970421,"condition_number":112996.76235537097}},{"element_name":"Tet4","epsilon_index":0,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.03561327953985008,"max_eigval":4.5159897288327,"condition_number":1361.0089214612253},"mass_condition":{"min_eigval":0.5097354288165642,"max_eigval":2.450146526512886,"condition_number":4.806702434243799},"system_condition":{"min_eigval":-0.023198814543804545,"max_eigval":4.49048692970421,"condition_number":112996.76235537097}},{"element_name":"Tet10","epsilon_index":0,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-114037.0331038261,"max_eigval":23465288.0043721,"condition_number":17568.495059848075},"mass_condition":{"min_eigval":0.13866695954001637,"max_eigval":9.282037368378827,"condition_number":66.93762810671727},"system_condition":{"min_eigval":-30.46538527627954,"max_eigval":6521.079366106795,"condition_number":18427.568793219703}},{"element_name":"Tet10","epsilon_index":0,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.0691410400685376,"max_eigval":5.493234886945303,"condition_number":8165.486719514965},"mass_condition":{"min_eigval":0.2531949656547453,"max_eigval":4.29939707251273,"condition_number":16.980578825470623},"system_condition":{"min_eigval":-0.06659730000439779,"max_eigval":5.484991434911079,"condition_number":9697.106653188433}},{"element_name":"Tet10","epsilon_index":0,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.0691410400685376,"max_eigval":5.493234886945303,"condition_number":8165.486719514965},"mass_condition":{"min_eigval":0.2531949656547453,"max_eigval":4.29939707251273,"condition_number":16.980578825470623},"system_condition":{"min_eigval":-0.06659730000439779,"max_eigval":5.484991434911079,"condition_number":9697.106653188433}},{"element_name":"Hex8","epsilon_index":0,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-118455.42261236532,"max_eigval":11737827.292140154,"condition_number":3064.9025828703666},"mass_condition":{"min_eigval":0.6863319463202785,"max_eigval":29.14689087494684,"condition_number":42.467629594128454},"system_condition":{"min_eigval":-23.89882595806931,"max_eigval":3271.8073593768218,"condition_number":2014.928251098774}},{"element_name":"Hex8","epsilon_index":0,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.10605920318004963,"max_eigval":5.462814834194444,"condition_number":1471.6322454135307},"mass_condition":{"min_eigval":0.21947014912832452,"max_eigval":3.2524053226811103,"condition_number":14.819351677659927},"system_condition":{"min_eigval":-0.07827462456913495,"max_eigval":5.412483101178373,"condition_number":913.826682591498}},{"element_name":"Hex8","epsilon_index":0,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.10605920318004963,"max_eigval":5.462814834194444,"condition_number":1471.6322454135307},"mass_condition":{"min_eigval":0.21947014912832452,"max_eigval":3.2524053226811103,"condition_number":14.819351677659927},"system_condition":{"min_eigval":-0.07827462456913495,"max_eigval":5.412483101178373,"condition_number":913.826682591498}},{"element_name":"Hex20","epsilon_index":0,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-176043.6490424629,"max_eigval":38148756.93690972,"condition_number":57216.981420195785},"mass_condition":{"min_eigval":0.008234266958511476,"max_eigval":77.74316744561354,"condition_number":9441.419356127764},"system_condition":{"min_eigval":-47.451627334790125,"max_eigval":10654.922255530299,"condition_number":31128.981264522714}},{"element_name":"Hex20","epsilon_index":0,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.1179405082619155,"max_eigval":7.945289110081971,"condition_number":22677.7500107819},"mass_condition":{"min_eigval":0.0016404891461063342,"max_eigval":11.249701931389113,"condition_number":6857.529023029527},"system_condition":{"min_eigval":-0.11376402635692061,"max_eigval":7.95111575372152,"condition_number":13272.899189456335}},{"element_name":"Hex20","epsilon_index":0,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.1179405082619155,"max_eigval":7.945289110081971,"condition_number":22677.7500107819},"mass_condition":{"min_eigval":0.0016404891461063342,"max_eigval":11.249701931389113,"condition_number":6857.529023029527},"system_condition":{"min_eigval":-0.11376402635692061,"max_eigval":7.95111575372152,"condition_number":13272.899189456335}},{"element_name":"Tet4","epsilon_index":1,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-53592.972529974126,"max_eigval":27139702.060829774,"condition_number":1909.8046380675514},"mass_condition":{"min_eigval":0.034588550776347995,"max_eigval":28.176999952216335,"condition_number":814.6337247377261},"system_condition":{"min_eigval":-8.886318445141583,"max_eigval":7550.1562441467695,"condition_number":18682.451999053792}},{"element_name":"Tet4","epsilon_index":1,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.03496131497818868,"max_eigval":4.55160538527317,"condition_number":542.9501618632019},"mass_condition":{"min_eigval":0.42638177671856275,"max_eigval":2.55172626183288,"condition_number":5.984604411264909},"system_condition":{"min_eigval":-0.024271242032440743,"max_eigval":4.525756378894882,"condition_number":4275.430805507421}},{"element_name":"Tet4","epsilon_index":1,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.3384987182642506,"max_eigval":28.005406196341113,"condition_number":937.526748963984},"mass_condition":{"min_eigval":0.6070231638730058,"max_eigval":23.369598462140026,"condition_number":38.49869305321788},"system_condition":{"min_eigval":-0.19965837280293447,"max_eigval":27.92136131835746,"condition_number":6122.053551924961}},{"element_name":"Tet10","epsilon_index":1,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-105513.86173904134,"max_eigval":22899811.597583257,"condition_number":519922.9327071212},"mass_condition":{"min_eigval":0.00027139302255444603,"max_eigval":8.897728724077691,"condition_number":32785.399714145766},"system_condition":{"min_eigval":-28.01236362536283,"max_eigval":6363.901396945796,"condition_number":107433.67574619106}},{"element_name":"Tet10","epsilon_index":1,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.0715307205599254,"max_eigval":8.063046585942013,"condition_number":40975.73857049561},"mass_condition":{"min_eigval":0.024449684177153312,"max_eigval":5.143206763931423,"condition_number":210.3588220880753},"system_condition":{"min_eigval":-0.06893666796568947,"max_eigval":8.055520278953892,"condition_number":9933.029121824558}},{"element_name":"Tet10","epsilon_index":1,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-1.0355172964948327,"max_eigval":20.904880667806392,"condition_number":11505.173620012167},"mass_condition":{"min_eigval":0.2842889284165501,"max_eigval":19.15542218587262,"condition_number":67.38012026203646},"system_condition":{"min_eigval":-1.004634309891833,"max_eigval":20.9005178203697,"condition_number":6428.958502762207}},{"element_name":"Hex8","epsilon_index":1,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-244115.6067375581,"max_eigval":54941468.78261076,"condition_number":7026.21126733541},"mass_condition":{"min_eigval":0.0694930844653881,"max_eigval":149.35598139080741,"condition_number":2149.2207827556717},"system_condition":{"min_eigval":-39.86525164982323,"max_eigval":15360.997642035667,"condition_number":14720.880846499696}},{"element_name":"Hex8","epsilon_index":1,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.05832815714487878,"max_eigval":5.106600958818655,"condition_number":1443.3225379771957},"mass_condition":{"min_eigval":0.23712076016463723,"max_eigval":2.559463925221345,"condition_number":10.793925944924698},"system_condition":{"min_eigval":-0.03751260206829956,"max_eigval":5.050039236037262,"condition_number":4209.435152247178}},{"element_name":"Hex8","epsilon_index":1,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.0711646437563192,"max_eigval":5.10613746301508,"condition_number":1367.6880092718297},"mass_condition":{"min_eigval":0.24107809758505142,"max_eigval":2.5594608639066214,"condition_number":10.616729141077005},"system_condition":{"min_eigval":-0.044528302649155076,"max_eigval":5.049576976870373,"condition_number":4093.109526615035}},{"element_name":"Hex20","epsilon_index":1,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-7603303.395136636,"max_eigval":826657196.046602,"condition_number":1185311.9361307533},"mass_condition":{"min_eigval":0.0017444265887149868,"max_eigval":1567.7499318343248,"condition_number":898719.3510901431},"system_condition":{"min_eigval":-1785.2344193789168,"max_eigval":230165.29657718123,"condition_number":964288.5491847097}},{"element_name":"Hex20","epsilon_index":1,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.16232901621760015,"max_eigval":10.662709277419077,"condition_number":143861.83586721608},"mass_condition":{"min_eigval":0.0009131117647435048,"max_eigval":8.70567090490869,"condition_number":9534.069367022264},"system_condition":{"min_eigval":-0.13729422844744482,"max_eigval":10.609879261923322,"condition_number":98211.97407684354}},{"element_name":"Hex20","epsilon_index":1,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-1.388694856101994,"max_eigval":10.292235258621707,"condition_number":130711.80990223617},"mass_condition":{"min_eigval":0.0010247638853194347,"max_eigval":8.705667154917505,"condition_number":8495.290749052709},"system_condition":{"min_eigval":-1.3836610685207629,"max_eigval":10.240399969782422,"condition_number":86665.58856939733}},{"element_name":"Tet4","epsilon_index":1,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-53593.00044973404,"max_eigval":27139705.375076752,"condition_number":1909.8034699881746},"mass_condition":{"min_eigval":0.03458971681528199,"max_eigval":28.177005261282375,"condition_number":814.6064164605583},"system_condition":{"min_eigval":-8.88632600659988,"max_eigval":7550.157166000959,"condition_number":18682.47960965656}},{"element_name":"Tet4","epsilon_index":1,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.034961314068134956,"max_eigval":4.551605273603844,"condition_number":542.9505166369296},"mass_condition":{"min_eigval":0.42638388058472954,"max_eigval":2.551725321299748,"condition_number":5.984572676153685},"system_condition":{"min_eigval":-0.024271235357404455,"max_eigval":4.525756267035533,"condition_number":4275.44400420383}},{"element_name":"Tet4","epsilon_index":1,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.3384986582216659,"max_eigval":28.00540430747622,"condition_number":937.5265123518554},"mass_condition":{"min_eigval":0.6070231933001523,"max_eigval":23.369592756130245,"condition_number":38.49868178689966},"system_condition":{"min_eigval":-0.19965825276980587,"max_eigval":27.92135943922836,"condition_number":6122.069197379526}},{"element_name":"Tet10","epsilon_index":1,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-105513.85123941264,"max_eigval":22899813.127054203,"condition_number":518948.04866162606},"mass_condition":{"min_eigval":0.00027172730312521084,"max_eigval":8.89772948314769,"condition_number":32745.06971074472},"system_condition":{"min_eigval":-28.01236036040242,"max_eigval":6363.901822279658,"condition_number":107473.56235898472}},{"element_name":"Tet10","epsilon_index":1,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.07153048605647638,"max_eigval":8.062990088890707,"condition_number":40899.80597519147},"mass_condition":{"min_eigval":0.024473356374986814,"max_eigval":5.143162765099711,"condition_number":210.15355173581017},"system_condition":{"min_eigval":-0.06893643147438143,"max_eigval":8.055463733287638,"condition_number":9936.586220309033}},{"element_name":"Tet10","epsilon_index":1,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-1.035461403154279,"max_eigval":20.904877491471936,"condition_number":11484.229364647514},"mass_condition":{"min_eigval":0.2842890656997712,"max_eigval":19.155412953850146,"condition_number":67.3800552501009},"system_condition":{"min_eigval":-1.004579077413121,"max_eigval":20.900514644284414,"condition_number":6431.069833853251}},{"element_name":"Hex8","epsilon_index":1,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-244115.65503631934,"max_eigval":54941474.38033581,"condition_number":7026.221192977275},"mass_condition":{"min_eigval":0.06949388199605672,"max_eigval":149.35599135485825,"condition_number":2149.196261094367},"system_condition":{"min_eigval":-39.86525990388856,"max_eigval":15360.999212630742,"condition_number":14720.941408490002}},{"element_name":"Hex8","epsilon_index":1,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.0583281641332095,"max_eigval":5.106600678771245,"condition_number":1443.3254195509096},"mass_condition":{"min_eigval":0.2371207502698757,"max_eigval":2.5594639251458355,"condition_number":10.793926395023705},"system_condition":{"min_eigval":-0.037512605121286034,"max_eigval":5.050038962495974,"condition_number":4209.4533527501435}},{"element_name":"Hex8","epsilon_index":1,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.07116465902082826,"max_eigval":5.106137183525,"condition_number":1367.6907215671224},"mass_condition":{"min_eigval":0.24107811933574075,"max_eigval":2.5594608638521663,"condition_number":10.616728182982454},"system_condition":{"min_eigval":-0.04452830803167539,"max_eigval":5.049576703888019,"condition_number":4093.127187741112}},{"element_name":"Hex20","epsilon_index":1,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-7603302.51395344,"max_eigval":826657200.7774612,"condition_number":1185132.5025188988},"mass_condition":{"min_eigval":0.0017448644062578998,"max_eigval":1567.7499392277703,"condition_number":898493.8506425404},"system_condition":{"min_eigval":-1785.2341868071148,"max_eigval":230165.29789747827,"condition_number":964215.8460005802}},{"element_name":"Hex20","epsilon_index":1,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.1623289891892739,"max_eigval":10.662706226453626,"condition_number":143844.20002647451},"mass_condition":{"min_eigval":0.0009132811412583387,"max_eigval":8.70567090448012,"condition_number":9532.301184370517},"system_condition":{"min_eigval":-0.13729420433385928,"max_eigval":10.609876209573665,"condition_number":98205.5297453767}},{"element_name":"Hex20","epsilon_index":1,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-1.3886969265406948,"max_eigval":10.292233683752073,"condition_number":130695.99954448103},"mass_condition":{"min_eigval":0.0010247775191083563,"max_eigval":8.705667154487148,"condition_number":8495.177726051037},"system_condition":{"min_eigval":-1.3836639212379314,"max_eigval":10.240398373991159,"condition_number":86659.98646926775}},{"element_name":"Tet4","epsilon_index":2,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-50086.69228480809,"max_eigval":26453519.46951411,"condition_number":6436.645889530907},"mass_condition":{"min_eigval":0.0011575149849998817,"max_eigval":26.94940463366304,"condition_number":23282.12159919968},"system_condition":{"min_eigval":-7.470830400667317,"max_eigval":7359.297952411547,"condition_number":59299.30834941638}},{"element_name":"Tet4","epsilon_index":2,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.033334897313764614,"max_eigval":4.556700555241326,"condition_number":456.91218628761874},"mass_condition":{"min_eigval":0.44346846789052674,"max_eigval":2.530527785692465,"condition_number":5.706218071669401},"system_condition":{"min_eigval":-0.022948726914511372,"max_eigval":4.530335340780865,"condition_number":8694.089538675053}},{"element_name":"Tet4","epsilon_index":2,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.30517715119821737,"max_eigval":28.439782704964106,"condition_number":1012.1834242343089},"mass_condition":{"min_eigval":0.6028927887694165,"max_eigval":23.679646184314983,"condition_number":39.27671159021201},"system_condition":{"min_eigval":-0.17032527704885597,"max_eigval":28.35266791333219,"condition_number":15457.593015108952}},{"element_name":"Tet10","epsilon_index":2,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-102189.21400822105,"max_eigval":22670378.125120223,"condition_number":21724996.8508597},"mass_condition":{"min_eigval":1.2484149518929996e-6,"max_eigval":8.750599071862878,"condition_number":7009367.405119706},"system_condition":{"min_eigval":-27.111310262160814,"max_eigval":6300.107556558107,"condition_number":5347494.173392122}},{"element_name":"Tet10","epsilon_index":2,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.07203262402951473,"max_eigval":8.620563549616376,"condition_number":136681.3024446449},"mass_condition":{"min_eigval":0.0016538994203807124,"max_eigval":4.648460270883537,"condition_number":2810.606384887362},"system_condition":{"min_eigval":-0.06942574404841391,"max_eigval":8.617003633208405,"condition_number":45146.917390136565}},{"element_name":"Tet10","epsilon_index":2,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.8865721019704957,"max_eigval":20.977883745793157,"condition_number":17791.554548068616},"mass_condition":{"min_eigval":0.14834820108411997,"max_eigval":19.187467188414168,"condition_number":129.34074729719188},"system_condition":{"min_eigval":-0.868679672940311,"max_eigval":20.973094694100485,"condition_number":7337.098437356572}},{"element_name":"Hex8","epsilon_index":2,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-142890.05423900625,"max_eigval":32767368.820873596,"condition_number":166774.9167577902},"mass_condition":{"min_eigval":0.00012518811986139287,"max_eigval":84.00508899082831,"condition_number":671030.8381005959},"system_condition":{"min_eigval":-26.293488837891044,"max_eigval":9152.14756750258,"condition_number":166862.17582742125}},{"element_name":"Hex8","epsilon_index":2,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.06552460984276268,"max_eigval":4.984631792940832,"condition_number":658.1595207317607},"mass_condition":{"min_eigval":0.15926281487357594,"max_eigval":2.5791006807367793,"condition_number":16.193991565351205},"system_condition":{"min_eigval":-0.04652541189595993,"max_eigval":4.934935951113247,"condition_number":1034.9780708309795}},{"element_name":"Hex8","epsilon_index":2,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.25618822783324496,"max_eigval":5.094199911580197,"condition_number":428.9815398301524},"mass_condition":{"min_eigval":0.18604352643748187,"max_eigval":4.0585374092362425,"condition_number":21.814988604831004},"system_condition":{"min_eigval":-0.15208062128799174,"max_eigval":5.033281658247434,"condition_number":789.116107961735}},{"element_name":"Hex20","epsilon_index":2,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-4367507.687834088,"max_eigval":480734712.30413073,"condition_number":190419343.86072508},"mass_condition":{"min_eigval":5.28626017313633e-7,"max_eigval":987.9986095102405,"condition_number":1868993536.3587346},"system_condition":{"min_eigval":-1023.61584774027,"max_eigval":134001.7686542059,"condition_number":549495140.9501519}},{"element_name":"Hex20","epsilon_index":2,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.21553241362732364,"max_eigval":11.842568667150942,"condition_number":110333.15334190948},"mass_condition":{"min_eigval":0.00008578387927123968,"max_eigval":8.715812371932124,"condition_number":101601.98449843512},"system_condition":{"min_eigval":-0.19837402978905205,"max_eigval":11.82090800746522,"condition_number":2198883.2627435084}},{"element_name":"Hex20","epsilon_index":2,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-14.28673362856141,"max_eigval":15.580327242460248,"condition_number":88768.66731086162},"mass_condition":{"min_eigval":0.0001829127002681412,"max_eigval":8.7160325687483,"condition_number":47651.325227668814},"system_condition":{"min_eigval":-18.376191010230034,"max_eigval":17.1305582885457,"condition_number":1533549.2230343588}},{"element_name":"Tet4","epsilon_index":2,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-50086.614873789455,"max_eigval":26453521.10031027,"condition_number":6436.270422532818},"mass_condition":{"min_eigval":0.00115870019085555,"max_eigval":26.94940750534335,"condition_number":23258.30936943637},"system_condition":{"min_eigval":-7.470813694895682,"max_eigval":7359.298405747029,"condition_number":59300.60588984126}},{"element_name":"Tet4","epsilon_index":2,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.03333488733467024,"max_eigval":4.556700108247541,"condition_number":456.9125562275372},"mass_condition":{"min_eigval":0.44348897030999307,"max_eigval":2.5305186204859154,"condition_number":5.705933608037908},"system_condition":{"min_eigval":-0.022948702041008892,"max_eigval":4.530334890389508,"condition_number":8694.304813571869}},{"element_name":"Tet4","epsilon_index":2,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.30517674456480764,"max_eigval":28.439777671657883,"condition_number":1012.1828249182613},"mass_condition":{"min_eigval":0.6028931416847083,"max_eigval":23.679588018047312,"condition_number":39.27659212025154},"system_condition":{"min_eigval":-0.17032476063821664,"max_eigval":28.35266288766136,"condition_number":15457.960709687732}},{"element_name":"Tet10","epsilon_index":2,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-102189.1731738539,"max_eigval":22670378.570899446,"condition_number":20224513.89369624},"mass_condition":{"min_eigval":1.6498326354810776e-6,"max_eigval":8.750599427422062,"condition_number":5303931.58629115},"system_condition":{"min_eigval":-27.11129825306604,"max_eigval":6300.10768048253,"condition_number":5460226.9207586}},{"element_name":"Tet10","epsilon_index":2,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.07202876582627779,"max_eigval":8.619563555620843,"condition_number":127342.85519518118},"mass_condition":{"min_eigval":0.0020649876827803903,"max_eigval":4.645128194938704,"condition_number":2249.4701705359807},"system_condition":{"min_eigval":-0.06942176201449045,"max_eigval":8.615999687990493,"condition_number":46169.697483382704}},{"element_name":"Tet10","epsilon_index":2,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.8852040706451552,"max_eigval":20.977854024644387,"condition_number":16705.69608385288},"mass_condition":{"min_eigval":0.1494520058109702,"max_eigval":19.187034242579735,"condition_number":128.38258100629218},"system_condition":{"min_eigval":-0.8672783182298412,"max_eigval":20.973064934756977,"condition_number":7443.851944405817}},{"element_name":"Hex8","epsilon_index":2,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-142890.03129391058,"max_eigval":32767371.890825074,"condition_number":166631.28837666727},"mass_condition":{"min_eigval":0.000126168799441043,"max_eigval":84.00509775855285,"condition_number":665815.1470943283},"system_condition":{"min_eigval":-26.293475652928592,"max_eigval":9152.148426362115,"condition_number":166716.24580554405}},{"element_name":"Hex8","epsilon_index":2,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.06552449751443384,"max_eigval":4.984631101432868,"condition_number":658.178607922761},"mass_condition":{"min_eigval":0.1592742318778464,"max_eigval":2.5790671010521575,"condition_number":16.192619927560816},"system_condition":{"min_eigval":-0.04652526643575906,"max_eigval":4.934935258160856,"condition_number":1034.9905351595005}},{"element_name":"Hex8","epsilon_index":2,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.25618609240546075,"max_eigval":5.094198446045456,"condition_number":428.9792834978007},"mass_condition":{"min_eigval":0.1860439816119117,"max_eigval":4.058537160945581,"condition_number":21.814933897790368},"system_condition":{"min_eigval":-0.15207902995845787,"max_eigval":5.033280292220608,"condition_number":789.1258616410364}},{"element_name":"Hex20","epsilon_index":2,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-4367506.8359210715,"max_eigval":480734720.1485084,"condition_number":184136849.9440184},"mass_condition":{"min_eigval":9.910411975741687e-7,"max_eigval":987.9986152258564,"condition_number":996929913.351978},"system_condition":{"min_eigval":-1023.6155959835869,"max_eigval":134001.77084476122,"condition_number":501293021.42437243}},{"element_name":"Hex20","epsilon_index":2,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.21553218930318785,"max_eigval":11.8423507193862,"condition_number":110442.48814330986},"mass_condition":{"min_eigval":0.00010570098423530974,"max_eigval":8.715812357666083,"condition_number":82457.24882053216},"system_condition":{"min_eigval":-0.19837356063695658,"max_eigval":11.820687452092745,"condition_number":2036734.5063642533}},{"element_name":"Hex20","epsilon_index":2,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-14.29031437148767,"max_eigval":15.57802670938517,"condition_number":88860.50572323594},"mass_condition":{"min_eigval":0.0001830159807029388,"max_eigval":8.716032473455787,"condition_number":47624.433888115804},"system_condition":{"min_eigval":-18.47907487551431,"max_eigval":17.224270605274786,"condition_number":1425874.7111883885}},{"element_name":"Tet4","epsilon_index":3,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-52397.22131474019,"max_eigval":26207164.932185337,"condition_number":48225.283269134496},"mass_condition":{"min_eigval":0.00003706806694567554,"max_eigval":26.38638663041679,"condition_number":711836.0574099239},"system_condition":{"min_eigval":-7.778718889713186,"max_eigval":7290.782287517442,"condition_number":109393.64033690449}},{"element_name":"Tet4","epsilon_index":3,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.031636015564245834,"max_eigval":4.543307384272606,"condition_number":412.1554042200018},"mass_condition":{"min_eigval":0.48102864144608,"max_eigval":2.491445082137222,"condition_number":5.179411094207154},"system_condition":{"min_eigval":-0.02101318827991,"max_eigval":4.5171627984573295,"condition_number":11258.767630792474}},{"element_name":"Tet4","epsilon_index":3,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.25569269988038773,"max_eigval":28.67465385959154,"condition_number":1062.9484969623345},"mass_condition":{"min_eigval":0.5988709610817186,"max_eigval":23.496051882291912,"condition_number":39.23391416383232},"system_condition":{"min_eigval":-0.13050463981011173,"max_eigval":28.584849512197103,"condition_number":23380.50958399663}},{"element_name":"Tet10","epsilon_index":3,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-102246.59632442577,"max_eigval":22586753.04879211,"condition_number":561008601.3164108},"mass_condition":{"min_eigval":7.75102774955631e-9,"max_eigval":8.676857936866325,"condition_number":1119446119.5630493},"system_condition":{"min_eigval":-27.14449419639905,"max_eigval":6276.86293601657,"condition_number":817371607.7232848}},{"element_name":"Tet10","epsilon_index":3,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.07296341776959295,"max_eigval":8.462477559482776,"condition_number":681321.2413504965},"mass_condition":{"min_eigval":0.0002325863816088745,"max_eigval":4.370556931194693,"condition_number":18791.112794146204},"system_condition":{"min_eigval":-0.07076405853696126,"max_eigval":8.461007831211958,"condition_number":764734.2340311774}},{"element_name":"Tet10","epsilon_index":3,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.7245408073601863,"max_eigval":20.807179542784958,"condition_number":11398.979832219216},"mass_condition":{"min_eigval":0.027482987423644432,"max_eigval":19.0394087290666,"condition_number":692.7707106792338},"system_condition":{"min_eigval":-0.7033750849276598,"max_eigval":20.802643805941003,"condition_number":7627.829363432539}},{"element_name":"Hex8","epsilon_index":3,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-89785.66543903862,"max_eigval":19988476.76520965,"condition_number":12793372.528230129},"mass_condition":{"min_eigval":2.645861343550329e-7,"max_eigval":45.46870133997409,"condition_number":171848390.5092406},"system_condition":{"min_eigval":-15.872157978618818,"max_eigval":5582.4487931344665,"condition_number":12846238.293741386}},{"element_name":"Hex8","epsilon_index":3,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.06434246155969411,"max_eigval":4.788346438648662,"condition_number":1812.9430413960104},"mass_condition":{"min_eigval":0.140090108215406,"max_eigval":2.6042925218539765,"condition_number":18.59012427807931},"system_condition":{"min_eigval":-0.046519402731007464,"max_eigval":4.740754370194172,"condition_number":3377.949467890294}},{"element_name":"Hex8","epsilon_index":3,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.9476918937104468,"max_eigval":10.40819572072121,"condition_number":543.9583130858387},"mass_condition":{"min_eigval":0.2922385686368256,"max_eigval":7.39644111508821,"condition_number":25.3095994467452},"system_condition":{"min_eigval":-0.6295030258826431,"max_eigval":10.284072118121284,"condition_number":919.0328299346289}},{"element_name":"Hex20","epsilon_index":3,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-2605702.2233468853,"max_eigval":292391701.81626046,"condition_number":101224542034.02782},"mass_condition":{"min_eigval":2.263954032655423e-10,"max_eigval":583.4957172511939,"condition_number":2577330232128.4487},"system_condition":{"min_eigval":-665.6083316564713,"max_eigval":81520.98741813583,"condition_number":95092590200.34143}},{"element_name":"Hex20","epsilon_index":3,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.2651249572198719,"max_eigval":11.33393778759857,"condition_number":546977.8511289705},"mass_condition":{"min_eigval":0.000013667419946081205,"max_eigval":8.73670980247192,"condition_number":639236.2155358338},"system_condition":{"min_eigval":-0.24526824456957214,"max_eigval":11.325928185794622,"condition_number":509169.16557934467}},{"element_name":"Hex20","epsilon_index":3,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-61.34098639515341,"max_eigval":57.121766143411115,"condition_number":50155.5245680029},"mass_condition":{"min_eigval":0.0011556446613803328,"max_eigval":9.438977003462606,"condition_number":8167.715664595758},"system_condition":{"min_eigval":-37.025621378944535,"max_eigval":32.433273408057666,"condition_number":13412.1651315434}},{"element_name":"Tet4","epsilon_index":3,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-52397.00975715779,"max_eigval":26207166.097393516,"condition_number":48201.317348740435},"mass_condition":{"min_eigval":0.000038274384886220334,"max_eigval":26.38638854631595,"condition_number":689400.7212592895},"system_condition":{"min_eigval":-7.778663926563212,"max_eigval":7290.78261133389,"condition_number":109383.99800982676}},{"element_name":"Tet4","epsilon_index":3,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.031635995732545476,"max_eigval":4.543306415049311,"condition_number":412.15577612698075},"mass_condition":{"min_eigval":0.4811224130748694,"max_eigval":2.4913889919257275,"condition_number":5.178285035617396},"system_condition":{"min_eigval":-0.021013148053237517,"max_eigval":4.51716182044793,"condition_number":11258.120012475203}},{"element_name":"Tet4","epsilon_index":3,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.2556921143181089,"max_eigval":28.674643390481663,"condition_number":1062.9475138073735},"mass_condition":{"min_eigval":0.5988739796099671,"max_eigval":23.495645113854025,"condition_number":39.23303718948718},"system_condition":{"min_eigval":-0.13050392027475283,"max_eigval":28.584839076686386,"condition_number":23379.12850766884}},{"element_name":"Tet10","epsilon_index":3,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-102246.52592610533,"max_eigval":22586753.28935194,"condition_number":653721908.8881434},"mass_condition":{"min_eigval":2.090769662446967e-7,"max_eigval":8.676858170005609,"condition_number":41500784.73900613},"system_condition":{"min_eigval":-27.14447372379118,"max_eigval":6276.863002878332,"condition_number":207287112.88182718}},{"element_name":"Tet10","epsilon_index":3,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.0729391006262595,"max_eigval":8.45690998634182,"condition_number":871624.4922642995},"mass_condition":{"min_eigval":0.0006940787889580443,"max_eigval":4.3460704678956,"condition_number":6261.63850132915},"system_condition":{"min_eigval":-0.07073895938375678,"max_eigval":8.455427161016996,"condition_number":198848.28329335948}},{"element_name":"Tet10","epsilon_index":3,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.7228476756936659,"max_eigval":20.80705781830536,"condition_number":29203.474039169116},"mass_condition":{"min_eigval":0.03226309908641695,"max_eigval":19.033360209159582,"condition_number":589.9420932309877},"system_condition":{"min_eigval":-0.7008775674921779,"max_eigval":20.802521821513047,"condition_number":5144.929275820712}},{"element_name":"Hex8","epsilon_index":3,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-89785.60120041898,"max_eigval":19988479.45336584,"condition_number":11577766.409452109},"mass_condition":{"min_eigval":1.345626927079233e-6,"max_eigval":45.4687094269386,"condition_number":33789981.83815426},"system_condition":{"min_eigval":-15.872134687836294,"max_eigval":5582.449542541222,"condition_number":11601511.388861336}},{"element_name":"Hex8","epsilon_index":3,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.06434083233048903,"max_eigval":4.788346177857734,"condition_number":1814.6825162575747},"mass_condition":{"min_eigval":0.14027334822476334,"max_eigval":2.6027405690116514,"condition_number":18.55477609931445},"system_condition":{"min_eigval":-0.04651751691980914,"max_eigval":4.740754114375925,"condition_number":3379.8839378219664}},{"element_name":"Hex8","epsilon_index":3,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.9476271034852902,"max_eigval":10.408172887744126,"condition_number":544.5408128593118},"mass_condition":{"min_eigval":0.29223860245717925,"max_eigval":7.396397469812543,"condition_number":25.309447169616522},"system_condition":{"min_eigval":-0.6294484507865667,"max_eigval":10.284047475058339,"condition_number":919.5380909963791}},{"element_name":"Hex20","epsilon_index":3,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-2605699.950958742,"max_eigval":292391709.42762125,"condition_number":9328639513.518343},"mass_condition":{"min_eigval":3.9063700951145963e-7,"max_eigval":583.4957220269914,"condition_number":1493703125.4584036},"system_condition":{"min_eigval":-665.6077083976088,"max_eigval":81520.98954285207,"condition_number":9357897588.347406}},{"element_name":"Hex20","epsilon_index":3,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.26512433129842017,"max_eigval":11.319592942942272,"condition_number":465277.7451886495},"mass_condition":{"min_eigval":0.00003877397827715685,"max_eigval":8.736708674495642,"condition_number":225324.0204563366},"system_condition":{"min_eigval":-0.24526760785952284,"max_eigval":11.311429653401934,"condition_number":333050.10515583464}},{"element_name":"Hex20","epsilon_index":3,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-55.07221292409817,"max_eigval":50.20501197527082,"condition_number":47888.14454621026},"mass_condition":{"min_eigval":0.0011848542916606422,"max_eigval":9.438805662453843,"condition_number":7966.216376888679},"system_condition":{"min_eigval":-37.16364379711554,"max_eigval":32.111943275820074,"condition_number":12667.466717786705}},{"element_name":"Tet4","epsilon_index":4,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-54920.17123307611,"max_eigval":26103495.005298514,"condition_number":377719.6752114921},"mass_condition":{"min_eigval":1.169193276401439e-6,"max_eigval":26.13710425663855,"condition_number":22354819.15965488},"system_condition":{"min_eigval":-8.340086022868544,"max_eigval":7261.9515750158025,"condition_number":378485.3404541757}},{"element_name":"Tet4","epsilon_index":4,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.03060978075889939,"max_eigval":4.52772327053977,"condition_number":389.1820856044988},"mass_condition":{"min_eigval":0.5021408667206809,"max_eigval":2.4577363256258726,"condition_number":4.894515639956873},"system_condition":{"min_eigval":-0.01962077255395403,"max_eigval":4.50186172351405,"condition_number":3797.44072851586}},{"element_name":"Tet4","epsilon_index":4,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.21899870007330013,"max_eigval":28.837638273228833,"condition_number":1091.899564244822},"mass_condition":{"min_eigval":0.5959011315671762,"max_eigval":23.142728049382967,"condition_number":38.83652307979227},"system_condition":{"min_eigval":-0.10420717178157056,"max_eigval":28.74409600896322,"condition_number":8638.641593054701}},{"element_name":"Tet10","epsilon_index":4,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-102909.3209701425,"max_eigval":22552576.540481627,"condition_number":19152157803.977516},"mass_condition":{"min_eigval":5.432402707088579e-11,"max_eigval":8.640334532720162,"condition_number":159051804488.7487},"system_condition":{"min_eigval":-27.34762848059637,"max_eigval":6267.364826748641,"condition_number":7714209960.259017}},{"element_name":"Tet10","epsilon_index":4,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.0723590817853711,"max_eigval":8.134646118140642,"condition_number":1970940.4251310388},"mass_condition":{"min_eigval":0.00004458186813154231,"max_eigval":4.277010519188483,"condition_number":95936.09910129443},"system_condition":{"min_eigval":-0.07028494313505147,"max_eigval":8.134237323156743,"condition_number":1933137.3428336035}},{"element_name":"Tet10","epsilon_index":4,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.6403851896220196,"max_eigval":20.635798081279685,"condition_number":6685.096486636635},"mass_condition":{"min_eigval":0.0058314571236182606,"max_eigval":18.97844038787613,"condition_number":3254.493685808072},"system_condition":{"min_eigval":-0.6307325496639324,"max_eigval":20.631160566808738,"condition_number":31934.95632376543}},{"element_name":"Hex8","epsilon_index":4,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-72830.0489078254,"max_eigval":12434592.278455,"condition_number":951926454.2413852},"mass_condition":{"min_eigval":5.742722173430239e-10,"max_eigval":28.971801035094924,"condition_number":50449595436.008194},"system_condition":{"min_eigval":-12.689762979700978,"max_eigval":3471.8546244168956,"condition_number":956557261.2998931}},{"element_name":"Hex8","epsilon_index":4,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.06779560376535403,"max_eigval":4.8239394958273465,"condition_number":2882.9325621407593},"mass_condition":{"min_eigval":0.13775775197144777,"max_eigval":2.623765716270692,"condition_number":19.04622918653975},"system_condition":{"min_eigval":-0.04840155449505162,"max_eigval":4.776083151393788,"condition_number":1527.5896970972003}},{"element_name":"Hex8","epsilon_index":4,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.8467646856231914,"max_eigval":10.830338401140562,"condition_number":795.8733361049539},"mass_condition":{"min_eigval":0.28896859834532923,"max_eigval":7.462376746395359,"condition_number":25.82417878318223},"system_condition":{"min_eigval":-0.5534331903250556,"max_eigval":10.702197191390024,"condition_number":869.9671762426545}},{"element_name":"Hex20","epsilon_index":4,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-1264948.807561284,"max_eigval":165093038.49616343,"condition_number":23941173374693.492},"mass_condition":{"min_eigval":1.1398176833072217e-13,"max_eigval":320.5919395151192,"condition_number":2812659815418970.5},"system_condition":{"min_eigval":-322.9716110072567,"max_eigval":46035.69906683645,"condition_number":24068114910265.207}},{"element_name":"Hex20","epsilon_index":4,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.24456577387626224,"max_eigval":10.423302157988083,"condition_number":2797664.7182444553},"mass_condition":{"min_eigval":2.8524622758706544e-6,"max_eigval":8.778248592027214,"condition_number":3077428.461117102},"system_condition":{"min_eigval":-0.22662100900392035,"max_eigval":10.419489771393732,"condition_number":1454486.0929829446}},{"element_name":"Hex20","epsilon_index":4,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-59.34246831216055,"max_eigval":53.004057870454666,"condition_number":148863.28019924162},"mass_condition":{"min_eigval":0.0002684421795479353,"max_eigval":9.61734836090515,"condition_number":35826.51719301734},"system_condition":{"min_eigval":-28.85883120506537,"max_eigval":30.014757530910202,"condition_number":45756.81557476257}},{"element_name":"Tet4","epsilon_index":4,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-54919.84370910576,"max_eigval":26103495.998202752,"condition_number":376199.03302142763},"mass_condition":{"min_eigval":2.3814446375456076e-6,"max_eigval":26.13710579646982,"condition_number":10975315.312560678},"system_condition":{"min_eigval":-8.339992239015364,"max_eigval":7261.951850914723,"condition_number":376938.0854604981}},{"element_name":"Tet4","epsilon_index":4,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.030609749470354282,"max_eigval":4.527721687701437,"condition_number":389.1825610897759},"mass_condition":{"min_eigval":0.5022430057799759,"max_eigval":2.4574850865350837,"condition_number":4.893020028658529},"system_condition":{"min_eigval":-0.019620723164754182,"max_eigval":4.501860126492796,"condition_number":3797.3388858978014}},{"element_name":"Tet4","epsilon_index":4,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.2189980426878726,"max_eigval":28.83761901607447,"condition_number":1091.8981085655282},"mass_condition":{"min_eigval":0.5959159270912114,"max_eigval":23.140601603572495,"condition_number":38.831990473096674},"system_condition":{"min_eigval":-0.10420639306226741,"max_eigval":28.7440769397987,"condition_number":8638.391965574168}},{"element_name":"Tet10","epsilon_index":4,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-102909.2320630285,"max_eigval":22552576.718855135,"condition_number":1807011610.7558684},"mass_condition":{"min_eigval":1.5096272754972292e-7,"max_eigval":8.640334718968639,"condition_number":57234887.44016468},"system_condition":{"min_eigval":-27.347602550616614,"max_eigval":6267.3648763229885,"condition_number":4024748173.4887223}},{"element_name":"Tet10","epsilon_index":4,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.07229748289769355,"max_eigval":8.08118435899698,"condition_number":1798998.1580349975},"mass_condition":{"min_eigval":0.0004682912779544752,"max_eigval":4.257166957639448,"condition_number":9090.852548514276},"system_condition":{"min_eigval":-0.07022091812156832,"max_eigval":8.080692045719939,"condition_number":4459811.536895939}},{"element_name":"Tet10","epsilon_index":4,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.4945932523916228,"max_eigval":20.635373212909464,"condition_number":9687.98094781266},"mass_condition":{"min_eigval":0.022463149726417338,"max_eigval":18.96672867284145,"condition_number":844.3485844078228},"system_condition":{"min_eigval":-0.47295288268136937,"max_eigval":20.63073504394548,"condition_number":10968.969214131348}},{"element_name":"Hex8","epsilon_index":4,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-72829.98632060998,"max_eigval":12434594.906606078,"condition_number":76031604.56210193},"mass_condition":{"min_eigval":7.570287996836457e-7,"max_eigval":28.971807207838705,"condition_number":38270416.1584681},"system_condition":{"min_eigval":-12.6897128860642,"max_eigval":3471.8553576782797,"condition_number":74565125.4352315}},{"element_name":"Hex8","epsilon_index":4,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.06778773794094066,"max_eigval":4.823939273662122,"condition_number":2898.0830828155945},"mass_condition":{"min_eigval":0.14208462817667522,"max_eigval":2.6232661644392397,"condition_number":18.462702110022327},"system_condition":{"min_eigval":-0.048390434262032045,"max_eigval":4.776082935066632,"condition_number":1533.7514890651028}},{"element_name":"Hex8","epsilon_index":4,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.8463766855021091,"max_eigval":10.830274454348698,"condition_number":800.2521150981592},"mass_condition":{"min_eigval":0.2889692569052936,"max_eigval":7.461309762427805,"condition_number":25.820427551132763},"system_condition":{"min_eigval":-0.5531071034287742,"max_eigval":10.702139008865895,"condition_number":873.1198868384645}},{"element_name":"Hex20","epsilon_index":4,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-1264946.1409880372,"max_eigval":165093045.98793,"condition_number":10886225155.01588},"mass_condition":{"min_eigval":1.4237544368842526e-7,"max_eigval":320.5919438954556,"condition_number":2251736223.5375347},"system_condition":{"min_eigval":-322.97087221980814,"max_eigval":46035.70115796129,"condition_number":13103484959.509735}},{"element_name":"Hex20","epsilon_index":4,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.24456368668841633,"max_eigval":10.265893689641686,"condition_number":539932.4390414347},"mass_condition":{"min_eigval":0.000014442221690899055,"max_eigval":8.778206414496253,"condition_number":607815.514979108},"system_condition":{"min_eigval":-0.22661874937951398,"max_eigval":10.260694360542464,"condition_number":558319.3749933651}},{"element_name":"Hex20","epsilon_index":4,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-45.43847348032221,"max_eigval":39.02202636413547,"condition_number":33206.84547773183},"mass_condition":{"min_eigval":0.0004515810550144812,"max_eigval":9.616107800269893,"condition_number":21294.31182617155},"system_condition":{"min_eigval":-32.33531637065437,"max_eigval":30.02088721945986,"condition_number":40236.558705447846}},{"element_name":"Tet4","epsilon_index":8,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-62882.33875273703,"max_eigval":26016471.445129894,"condition_number":1522252999.565265},"mass_condition":{"min_eigval":1.1215305753207252e-12,"max_eigval":25.925898985165958,"condition_number":23116533383632.363},"system_condition":{"min_eigval":-9.724260114443831,"max_eigval":7237.750841420424,"condition_number":1524559871.754039}},{"element_name":"Tet4","epsilon_index":8,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.0298831594370507,"max_eigval":4.502325961848571,"condition_number":421.72695098645175},"mass_condition":{"min_eigval":0.5159223070493008,"max_eigval":2.4140517149081324,"condition_number":4.679099317714614},"system_condition":{"min_eigval":-0.018256888967938548,"max_eigval":4.476834294424577,"condition_number":1993.9340435462477}},{"element_name":"Tet4","epsilon_index":8,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.17911425134429543,"max_eigval":29.118303836382932,"condition_number":1124.5756262323814},"mass_condition":{"min_eigval":0.5921771783009179,"max_eigval":22.432536157304384,"condition_number":37.88146010906414},"system_condition":{"min_eigval":-0.0782937168273541,"max_eigval":29.012689334753357,"condition_number":4994.9753996375775}},{"element_name":"Tet10","epsilon_index":8,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-104545.00819980881,"max_eigval":22524822.05662057,"condition_number":1.588132228083256e16},"mass_condition":{"min_eigval":-3.113853070361336e-16,"max_eigval":8.607220537199167,"condition_number":4.692967981191248e19},"system_condition":{"min_eigval":-27.812595195394636,"max_eigval":6259.652281694463,"condition_number":1.5889394306082418e16}},{"element_name":"Tet10","epsilon_index":8,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.06880046070810435,"max_eigval":7.2789741258730745,"condition_number":375804841.618646},"mass_condition":{"min_eigval":1.3744445724830259e-7,"max_eigval":4.259750639389039,"condition_number":30992523.996027827},"system_condition":{"min_eigval":-0.06624112002554096,"max_eigval":7.278972799480606,"condition_number":375826362.4758464}},{"element_name":"Tet10","epsilon_index":8,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.7791757707241294,"max_eigval":20.636334611675455,"condition_number":123633.07030558825},"mass_condition":{"min_eigval":1.595377891243046e-6,"max_eigval":19.305758857512846,"condition_number":12101057.0370075},"system_condition":{"min_eigval":-0.7787586328569959,"max_eigval":20.630710167991754,"condition_number":123599.66461038869}},{"element_name":"Hex8","epsilon_index":8,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-83322.41552288109,"max_eigval":10344601.895090245,"condition_number":1.0245776062763078e17},"mass_condition":{"min_eigval":-1.511107071038887e-16,"max_eigval":25.037126625702744,"condition_number":3.2794516585286133e20},"system_condition":{"min_eigval":-16.237789386456015,"max_eigval":2883.9125945150645,"condition_number":8.992839095010059e16}},{"element_name":"Hex8","epsilon_index":8,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.10559687121588882,"max_eigval":5.253045957508367,"condition_number":5144.071355794736},"mass_condition":{"min_eigval":0.20637121287402113,"max_eigval":2.9990236280484206,"condition_number":14.532180076293724},"system_condition":{"min_eigval":-0.08220052419052087,"max_eigval":5.202709725018375,"condition_number":386.72021518068783}},{"element_name":"Hex8","epsilon_index":8,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.8254327784472743,"max_eigval":12.803178309533587,"condition_number":190402.1042153673},"mass_condition":{"min_eigval":3.951819510380275e-9,"max_eigval":8.943735721543517,"condition_number":2263194383.764475},"system_condition":{"min_eigval":-0.5717229076248467,"max_eigval":12.708151352596346,"condition_number":191046.62513986396}},{"element_name":"Hex20","epsilon_index":8,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-165315.8841253441,"max_eigval":34116739.66061506,"condition_number":1.30642940377711e22},"mass_condition":{"min_eigval":-1.4600978386894575e-15,"max_eigval":69.35351434634197,"condition_number":2.1795320911580516e26},"system_condition":{"min_eigval":-44.4692500874973,"max_eigval":9528.373450634399,"condition_number":1.3133167570431987e22}},{"element_name":"Hex20","epsilon_index":8,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.10690947970083736,"max_eigval":8.611128227076431,"condition_number":565552059.0417343},"mass_condition":{"min_eigval":9.741213987085844e-9,"max_eigval":9.57788852636473,"condition_number":983233561.9628478},"system_condition":{"min_eigval":-0.10276747127760111,"max_eigval":8.610052486478802,"condition_number":565476913.4912385}},{"element_name":"Hex20","epsilon_index":8,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-2.401688799507851,"max_eigval":10.663434563770897,"condition_number":481601579775.6322},"mass_condition":{"min_eigval":1.2401330716553961e-15,"max_eigval":12.417091779509423,"condition_number":1.0012709170746026e16},"system_condition":{"min_eigval":-2.0451965518469977,"max_eigval":10.643382382129477,"condition_number":483519843775.7705}},{"element_name":"Tet4","epsilon_index":8,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-62881.67180578479,"max_eigval":26016472.3045346,"condition_number":86694166.00946587},"mass_condition":{"min_eigval":1.0406120021260548e-6,"max_eigval":25.92590023355143,"condition_number":24914089.190382883},"system_condition":{"min_eigval":-9.724114159822566,"max_eigval":7237.751080199918,"condition_number":85528703.07351588}},{"element_name":"Tet4","epsilon_index":8,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.02988309723640786,"max_eigval":4.5023231398643,"condition_number":421.72770448145866},"mass_condition":{"min_eigval":0.5173536304837173,"max_eigval":2.408956635037575,"condition_number":4.656305654577584},"system_condition":{"min_eigval":-0.018256822322633502,"max_eigval":4.476831448744502,"condition_number":1993.8960488753025}},{"element_name":"Tet4","epsilon_index":8,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.17911343539437988,"max_eigval":29.118246807328074,"condition_number":1124.5725599302752},"mass_condition":{"min_eigval":0.5923357331786168,"max_eigval":22.382239606670755,"condition_number":37.78640786461125},"system_condition":{"min_eigval":-0.07829288965503692,"max_eigval":29.012633166564864,"condition_number":4994.862903846329}},{"element_name":"Tet10","epsilon_index":8,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-104544.80971889,"max_eigval":22524822.19274359,"condition_number":5660816892.449467},"mass_condition":{"min_eigval":1.392755341650118e-7,"max_eigval":8.607220687882537,"condition_number":61799947.417073384},"system_condition":{"min_eigval":-27.812537849109503,"max_eigval":6259.652319525016,"condition_number":7933587347.595562}},{"element_name":"Tet10","epsilon_index":8,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.06879948776633837,"max_eigval":6.3340734772933684,"condition_number":318323.3417084701},"mass_condition":{"min_eigval":0.2554486198590381,"max_eigval":4.252210133772402,"condition_number":16.646048571798353},"system_condition":{"min_eigval":-0.06624000807395172,"max_eigval":6.33386293888591,"condition_number":262579.58433026075}},{"element_name":"Tet10","epsilon_index":8,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.33668408514990844,"max_eigval":20.636147701783777,"condition_number":16747.635794193837},"mass_condition":{"min_eigval":0.2763401805548031,"max_eigval":19.28624844068429,"condition_number":69.79169081370522},"system_condition":{"min_eigval":-0.3197889836155571,"max_eigval":20.63052298423323,"condition_number":24821.866643277874}},{"element_name":"Hex8","epsilon_index":8,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-83322.13131559046,"max_eigval":10344602.274124982,"condition_number":274943188.89487636},"mass_condition":{"min_eigval":2.1079753670245402e-7,"max_eigval":25.037128048844217,"condition_number":118773342.61350855},"system_condition":{"min_eigval":-16.237707174576343,"max_eigval":2883.912700298025,"condition_number":234903225.4654493}},{"element_name":"Hex8","epsilon_index":8,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.10559143689609554,"max_eigval":5.253044136622081,"condition_number":5240.618227286848},"mass_condition":{"min_eigval":0.09438008143369572,"max_eigval":2.997828407775022,"condition_number":31.763358986727177},"system_condition":{"min_eigval":-0.0821948666149895,"max_eigval":5.202707884340923,"condition_number":386.16094781958463}},{"element_name":"Hex8","epsilon_index":8,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.8252286652133942,"max_eigval":12.802983947405789,"condition_number":3728.8866747981633},"mass_condition":{"min_eigval":0.27450386465091753,"max_eigval":8.856720058982576,"condition_number":32.264464000335785},"system_condition":{"min_eigval":-0.571555433786241,"max_eigval":12.707962037835845,"condition_number":284.65447978588884}},{"element_name":"Hex20","epsilon_index":8,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-165315.7854804896,"max_eigval":34116741.74354542,"condition_number":5666085539.129561},"mass_condition":{"min_eigval":7.680259916327775e-8,"max_eigval":69.35351924959227,"condition_number":903010054.414315},"system_condition":{"min_eigval":-44.469224761834255,"max_eigval":9528.374033952088,"condition_number":17330060145.742203}},{"element_name":"Hex20","epsilon_index":8,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.10690905545496623,"max_eigval":8.47667717107706,"condition_number":12241340.754463458},"mass_condition":{"min_eigval":0.010019562091103225,"max_eigval":9.566796938387032,"condition_number":954.8118821362242},"system_condition":{"min_eigval":-0.10276708675188632,"max_eigval":8.461408971645724,"condition_number":12366013.538799703}},{"element_name":"Hex20","epsilon_index":8,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-2.399664343137493,"max_eigval":10.661662680642857,"condition_number":729160.6266387082},"mass_condition":{"min_eigval":0.01884894534489864,"max_eigval":12.275439230539158,"condition_number":651.2533728504569},"system_condition":{"min_eigval":-2.0435512935874973,"max_eigval":10.641698284733293,"condition_number":703770.5820656153}},{"element_name":"Tet4","epsilon_index":12,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-65168.22955117088,"max_eigval":26011334.194872536,"condition_number":6229344675602.479},"mass_condition":{"min_eigval":-7.400936420611099e-16,"max_eigval":25.913437921233815,"condition_number":8.066658920697494e18},"system_condition":{"min_eigval":-10.244582956901418,"max_eigval":7236.322213647596,"condition_number":6239035898697.098}},{"element_name":"Tet4","epsilon_index":12,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.029854121221621656,"max_eigval":4.500165530584753,"condition_number":424.42782757488595},"mass_condition":{"min_eigval":0.5168935092746367,"max_eigval":2.410895244753752,"condition_number":4.664201042371362},"system_condition":{"min_eigval":-0.018180142044911675,"max_eigval":4.474699246189306,"condition_number":1924.3851253055043}},{"element_name":"Tet4","epsilon_index":12,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.17651891013184864,"max_eigval":29.146562049239577,"condition_number":1127.0410056086946},"mass_condition":{"min_eigval":1.7325651339229563e-6,"max_eigval":22.366801398180318,"condition_number":12909645.334681498},"system_condition":{"min_eigval":-0.07670388259732881,"max_eigval":29.039804407681462,"condition_number":4852.699474254146}},{"element_name":"Tet10","epsilon_index":12,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-108156.21567370769,"max_eigval":22523219.21441203,"condition_number":1.4820588951220611e22},"mass_condition":{"min_eigval":-9.176693401435757e-16,"max_eigval":8.605208810804964,"condition_number":1.2670815347740878e28},"system_condition":{"min_eigval":-28.875872231103234,"max_eigval":6259.20689935422,"condition_number":1.6860056843093176e22}},{"element_name":"Tet10","epsilon_index":12,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.06878395750744208,"max_eigval":7.170975815303649,"condition_number":93825353305.73447},"mass_condition":{"min_eigval":5.292114812112299e-10,"max_eigval":4.261581301322423,"condition_number":8052700012.419896},"system_condition":{"min_eigval":-0.06622281087751472,"max_eigval":7.170975810673424,"condition_number":93826261903.75496}},{"element_name":"Tet10","epsilon_index":12,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.32881059902540755,"max_eigval":20.644471711241547,"condition_number":12108339684.947853},"mass_condition":{"min_eigval":5.460450011824232e-15,"max_eigval":19.35414808254792,"condition_number":3544423635531473.0},"system_condition":{"min_eigval":-0.312128639256536,"max_eigval":20.6388426444423,"condition_number":12110710520.596872}},{"element_name":"Hex8","epsilon_index":12,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-105760.78123641542,"max_eigval":10322604.841680255,"condition_number":2.7103400525090394e17},"mass_condition":{"min_eigval":-7.259414357946904e-16,"max_eigval":24.936454534949647,"condition_number":4.344348487640285e22},"system_condition":{"min_eigval":-22.742544253216124,"max_eigval":2877.6862566255004,"condition_number":2.4310151809218253e18}},{"element_name":"Hex8","epsilon_index":12,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.11599038234597114,"max_eigval":5.436000979229954,"condition_number":14608.189663888988},"mass_condition":{"min_eigval":0.222866526482641,"max_eigval":3.172072012397069,"condition_number":14.233057168610474},"system_condition":{"min_eigval":-0.09160298894054422,"max_eigval":5.386614400058794,"condition_number":353.5683139693633}},{"element_name":"Hex8","epsilon_index":12,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.9391325597137131,"max_eigval":13.182713110374726,"condition_number":44954846087403.02},"mass_condition":{"min_eigval":-3.094810506733385e-16,"max_eigval":9.56133501705969,"condition_number":6.5659238092758616e16},"system_condition":{"min_eigval":-0.6552660912984739,"max_eigval":13.095380717200243,"condition_number":45038719209392.32}},{"element_name":"Hex20","epsilon_index":12,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-168080.0673982161,"max_eigval":33539352.687817983,"condition_number":5.519027449864671e29},"mass_condition":{"min_eigval":-6.927286448073164e-15,"max_eigval":67.2258596544491,"condition_number":1.2030534690284847e35},"system_condition":{"min_eigval":-45.30491943341731,"max_eigval":9366.229896521418,"condition_number":2.1456429529559648e30}},{"element_name":"Hex20","epsilon_index":12,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.11735332642141509,"max_eigval":8.42780959164667,"condition_number":671677711205.0356},"mass_condition":{"min_eigval":3.7756669860455656e-11,"max_eigval":10.800228246547254,"condition_number":286048221055.0789},"system_condition":{"min_eigval":-0.1132839007660376,"max_eigval":8.427808746013643,"condition_number":673037693145.5327}},{"element_name":"Hex20","epsilon_index":12,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-39.92800031016655,"max_eigval":44.18723687315419,"condition_number":6.655714986968028e17},"mass_condition":{"min_eigval":-5.674904349744106e-16,"max_eigval":13.919256841060612,"condition_number":2.1221670945975334e18},"system_condition":{"min_eigval":-8.27777162413327,"max_eigval":13.203824687334466,"condition_number":2.6128075371994967e18}},{"element_name":"Tet4","epsilon_index":12,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-65167.25640284713,"max_eigval":26011335.046718545,"condition_number":123714253.06151728},"mass_condition":{"min_eigval":1.038009701284605e-6,"max_eigval":25.913439153265145,"condition_number":24964544.282385383},"system_condition":{"min_eigval":-10.244365267328739,"max_eigval":7236.3224503256315,"condition_number":118364085.93968524}},{"element_name":"Tet4","epsilon_index":12,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.029854070442298396,"max_eigval":4.500163210826902,"condition_number":424.42803321444467},"mass_condition":{"min_eigval":0.5170142840008761,"max_eigval":2.41052232030766,"condition_number":4.662390179346718},"system_condition":{"min_eigval":-0.01818009319761117,"max_eigval":4.474696909294004,"condition_number":1924.3639434063205}},{"element_name":"Tet4","epsilon_index":12,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.17651838918713073,"max_eigval":29.146506610014853,"condition_number":1127.038107191151},"mass_condition":{"min_eigval":0.5919164679095549,"max_eigval":22.363200716442115,"condition_number":37.781007842916146},"system_condition":{"min_eigval":-0.07670321409237854,"max_eigval":29.039749694365447,"condition_number":4852.627725464807}},{"element_name":"Tet10","epsilon_index":12,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-108155.25089521834,"max_eigval":22523219.34831096,"condition_number":7749099250.041612},"mass_condition":{"min_eigval":1.391962282062518e-7,"max_eigval":8.60520895953341,"condition_number":61820704.9890948},"system_condition":{"min_eigval":-28.875593308168064,"max_eigval":6259.206936566647,"condition_number":18003909884.425045}},{"element_name":"Tet10","epsilon_index":12,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.06878365620641756,"max_eigval":5.499469414915237,"condition_number":31490.466658401867},"mass_condition":{"min_eigval":0.25568898534760715,"max_eigval":4.2607797922697115,"condition_number":16.663916071618083},"system_condition":{"min_eigval":-0.06622246598551092,"max_eigval":5.491213599437471,"condition_number":54074.487109450674}},{"element_name":"Tet10","epsilon_index":12,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.7353513414489304,"max_eigval":20.64439303325171,"condition_number":6913.632639614756},"mass_condition":{"min_eigval":0.2759423061229853,"max_eigval":19.35263015699517,"condition_number":70.13288548936694},"system_condition":{"min_eigval":-0.6831924518786544,"max_eigval":20.63876393337618,"condition_number":17865.649895519193}},{"element_name":"Hex8","epsilon_index":12,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-105759.02063901338,"max_eigval":10322605.192447532,"condition_number":281581268.4958876},"mass_condition":{"min_eigval":1.106318412633346e-9,"max_eigval":24.93645584208739,"condition_number":22540035090.55922},"system_condition":{"min_eigval":-22.742055232009193,"max_eigval":2877.686354479921,"condition_number":238630926.93380374}},{"element_name":"Hex8","epsilon_index":12,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.11598981155696518,"max_eigval":5.435994798360232,"condition_number":14621.597270235674},"mass_condition":{"min_eigval":0.00037386946153140514,"max_eigval":3.1706966941027264,"condition_number":8480.75871486066},"system_condition":{"min_eigval":-0.09160229806413737,"max_eigval":5.386608275670644,"condition_number":353.5663324564099}},{"element_name":"Hex8","epsilon_index":12,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.9390724030647416,"max_eigval":13.182669992997393,"condition_number":11188.357940706614},"mass_condition":{"min_eigval":0.003297901204337066,"max_eigval":9.53727772972659,"condition_number":2891.923420017594},"system_condition":{"min_eigval":-0.6552228179322315,"max_eigval":13.095337551504045,"condition_number":281.7593500462045}},{"element_name":"Hex20","epsilon_index":12,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-168079.90329086548,"max_eigval":33539354.01426966,"condition_number":7533400684.7776165},"mass_condition":{"min_eigval":3.314698315099485e-8,"max_eigval":67.22586282460338,"condition_number":2028114067.526707},"system_condition":{"min_eigval":-45.30487549780633,"max_eigval":9366.230267772788,"condition_number":3866893279.553661}},{"element_name":"Hex20","epsilon_index":12,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.11734950075038657,"max_eigval":7.843940906831145,"condition_number":2462988.321941854},"mass_condition":{"min_eigval":0.008644355788089773,"max_eigval":10.769933111029502,"condition_number":1245.891929375276},"system_condition":{"min_eigval":-0.11327999045679031,"max_eigval":7.84686675172915,"condition_number":1853599.2182924286}},{"element_name":"Hex20","epsilon_index":12,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-38.96179887523798,"max_eigval":43.22197551093992,"condition_number":834720.4282295266},"mass_condition":{"min_eigval":0.010239491529633934,"max_eigval":13.700120080974516,"condition_number":1337.9687889115623},"system_condition":{"min_eigval":-8.262958943621385,"max_eigval":13.19155913922084,"condition_number":198705.5775027584}},{"element_name":"Tet4","epsilon_index":16,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-65360.15913342742,"max_eigval":26011014.248738423,"condition_number":3.422324907768481e16},"mass_condition":{"min_eigval":-3.5451337727117853e-15,"max_eigval":25.91266199730223,"condition_number":8.410630506003697e24},"system_condition":{"min_eigval":-10.295527095095236,"max_eigval":7236.233239238487,"condition_number":3.115452943115447e16}},{"element_name":"Tet4","epsilon_index":16,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.02985237500843747,"max_eigval":4.500027445886317,"condition_number":424.6000349328027},"mass_condition":{"min_eigval":0.5169545178981578,"max_eigval":2.41069629523438,"condition_number":4.663265745380905},"system_condition":{"min_eigval":-0.018175402951771925,"max_eigval":4.474562751981244,"condition_number":1920.1442023525792}},{"element_name":"Tet4","epsilon_index":16,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.17635645727635688,"max_eigval":29.14838820844073,"condition_number":32555.245321797567},"mass_condition":{"min_eigval":1.6538609855320395e-12,"max_eigval":22.362352260229812,"condition_number":13521301037907.94},"system_condition":{"min_eigval":-0.07660478845532737,"max_eigval":29.041561907131015,"condition_number":32499.241976731606}},{"element_name":"Tet10","epsilon_index":16,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-109127.3001459776,"max_eigval":22523119.540205624,"condition_number":1.516323065339661e26},"mass_condition":{"min_eigval":-6.11025843035646e-16,"max_eigval":8.605083353023902,"condition_number":7.597612511975664e36},"system_condition":{"min_eigval":-29.150692148807103,"max_eigval":6259.179202931189,"condition_number":1.5205300664334251e26}},{"element_name":"Tet10","epsilon_index":16,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.06878303706126346,"max_eigval":7.163734286021853,"condition_number":23987489842481.895},"mass_condition":{"min_eigval":2.0637573111607337e-12,"max_eigval":4.261709342264512,"condition_number":2065024467371.8772},"system_condition":{"min_eigval":-0.06622183787410268,"max_eigval":7.163734286003879,"condition_number":23983076408431.75}},{"element_name":"Tet10","epsilon_index":16,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.3283084136918131,"max_eigval":20.644970622185966,"condition_number":1.2512261762026588e16},"mass_condition":{"min_eigval":-2.4027939493766214e-15,"max_eigval":19.357234082311436,"condition_number":8.563155995593912e23},"system_condition":{"min_eigval":-0.31163799112794444,"max_eigval":20.639341554931953,"condition_number":1.2586624947926256e16}},{"element_name":"Hex8","epsilon_index":16,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-109661.8179792398,"max_eigval":10321300.826395528,"condition_number":2.949821996767495e20},"mass_condition":{"min_eigval":-2.393396290298235e-15,"max_eigval":24.930432746203905,"condition_number":3.1080256345021994e29},"system_condition":{"min_eigval":-23.84110823957564,"max_eigval":2877.317031377382,"condition_number":2.7501724227865634e20}},{"element_name":"Hex8","epsilon_index":16,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.11681256701817973,"max_eigval":5.459327503746276,"condition_number":12194.024142448085},"mass_condition":{"min_eigval":0.22394844471451225,"max_eigval":3.1870290225316076,"condition_number":14.23108352725739},"system_condition":{"min_eigval":-0.09234100816014376,"max_eigval":5.409920359766247,"condition_number":352.6134937520516}},{"element_name":"Hex8","epsilon_index":16,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.9538746200992928,"max_eigval":13.210730346955678,"condition_number":9.658739186765154e16},"mass_condition":{"min_eigval":-5.385239126551054e-16,"max_eigval":9.61304070593424,"condition_number":2.1666631818036262e17},"system_condition":{"min_eigval":-0.6651962634793511,"max_eigval":13.123884237889397,"condition_number":8.412014459916637e16}},{"element_name":"Hex20","epsilon_index":16,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-172803.5165035834,"max_eigval":33513024.220377997,"condition_number":1.058778776643879e33},"mass_condition":{"min_eigval":-2.105831577122927e-15,"max_eigval":67.1242193660545,"condition_number":5.654235846216014e40},"system_condition":{"min_eigval":-46.5985529122171,"max_eigval":9358.833431747926,"condition_number":8.348911552311716e32}},{"element_name":"Hex20","epsilon_index":16,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.11969838391716564,"max_eigval":8.417043935793142,"condition_number":113826953532524.58},"mass_condition":{"min_eigval":1.4771431794430304e-13,"max_eigval":11.002795429671389,"condition_number":74486993426189.66},"system_condition":{"min_eigval":-0.11568369435117641,"max_eigval":8.41704393281891,"condition_number":113096546432538.52}},{"element_name":"Hex20","epsilon_index":16,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-6.582458390400296,"max_eigval":12.255571210842282,"condition_number":2.492766339164895e18},"mass_condition":{"min_eigval":-1.8253988024345536e-15,"max_eigval":14.13231373976027,"condition_number":5.454508645470238e24},"system_condition":{"min_eigval":-7.914848561851994,"max_eigval":12.368208317047836,"condition_number":2.2051407991113672e18}},{"element_name":"Tet4","epsilon_index":16,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-65359.14674562238,"max_eigval":26011015.100114975,"condition_number":135602067.85478032},"mass_condition":{"min_eigval":1.0380089770930463e-6,"max_eigval":25.91266322831853,"condition_number":24963814.186739676},"system_condition":{"min_eigval":-10.295296649046737,"max_eigval":7236.2334757859635,"condition_number":128168113.38381618}},{"element_name":"Tet4","epsilon_index":16,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.029852335862473132,"max_eigval":4.500025760681943,"condition_number":424.6001369492907},"mass_condition":{"min_eigval":0.5169620595121118,"max_eigval":2.410673067863194,"condition_number":4.663152785599568},"system_condition":{"min_eigval":-0.018175368217810236,"max_eigval":4.474561057137663,"condition_number":1920.1268307333553}},{"element_name":"Tet4","epsilon_index":16,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.1763562149069072,"max_eigval":29.14834686230675,"condition_number":1127.1951439518175},"mass_condition":{"min_eigval":0.5918883415717929,"max_eigval":22.36235110066419,"condition_number":37.781367751356115},"system_condition":{"min_eigval":-0.0766042453387481,"max_eigval":29.04152113183105,"condition_number":4843.95880780938}},{"element_name":"Tet10","epsilon_index":16,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-109126.04776822151,"max_eigval":22523119.673966922,"condition_number":17748178561.87029},"mass_condition":{"min_eigval":1.3919619217852191e-7,"max_eigval":8.605083501631189,"condition_number":61819819.687272735},"system_condition":{"min_eigval":-29.150339567949015,"max_eigval":6259.179240105406,"condition_number":50595329006.6451}},{"element_name":"Tet10","epsilon_index":16,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.06878283579895422,"max_eigval":5.4995267191582435,"condition_number":11014.701844954907},"mass_condition":{"min_eigval":0.2556121525556048,"max_eigval":4.2616592412738115,"condition_number":16.672365529830383},"system_condition":{"min_eigval":-0.06622160890364959,"max_eigval":5.491266486847075,"condition_number":31941.434030856457}},{"element_name":"Tet10","epsilon_index":16,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.9532267600007194,"max_eigval":20.644930090346975,"condition_number":6913.150874424658},"mass_condition":{"min_eigval":0.2759100203894865,"max_eigval":19.357230166256826,"condition_number":70.15776425564873},"system_condition":{"min_eigval":-0.8905337158126483,"max_eigval":20.63930103795766,"condition_number":17702.056050739546}},{"element_name":"Hex8","epsilon_index":16,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-109659.73869061195,"max_eigval":10321301.175527263,"condition_number":296387354.56236607},"mass_condition":{"min_eigval":2.8051260981788335e-13,"max_eigval":24.930434046505077,"condition_number":88874557413624.34},"system_condition":{"min_eigval":-23.840534057851343,"max_eigval":2877.3171287731816,"condition_number":248380072.8978519}},{"element_name":"Hex8","epsilon_index":16,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.11681200784983875,"max_eigval":5.459320758517421,"condition_number":12209.830491202301},"mass_condition":{"min_eigval":9.471081386128265e-8,"max_eigval":3.186942537149349,"condition_number":33649193.86942526},"system_condition":{"min_eigval":-0.09234032840449567,"max_eigval":5.409913677901231,"condition_number":352.61681512000007}},{"element_name":"Hex8","epsilon_index":16,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.953814555870941,"max_eigval":13.210688202536817,"condition_number":9376.691465336846},"mass_condition":{"min_eigval":8.35972820214977e-7,"max_eigval":9.612957700025236,"condition_number":11499127.085917924},"system_condition":{"min_eigval":-0.665153146856341,"max_eigval":13.123842099512185,"condition_number":282.1231568644028}},{"element_name":"Hex20","epsilon_index":16,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-172801.9157190628,"max_eigval":33513025.515527867,"condition_number":9953454429.31893},"mass_condition":{"min_eigval":3.1561790806863986e-8,"max_eigval":67.12422245699865,"condition_number":2126755825.3507159},"system_condition":{"min_eigval":-46.5981115809054,"max_eigval":9358.833794219576,"condition_number":4183115914.342876}},{"element_name":"Hex20","epsilon_index":16,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.11969136392269655,"max_eigval":7.886734352633679,"condition_number":187360.22327360112},"mass_condition":{"min_eigval":0.0037062372048588736,"max_eigval":11.000634570535228,"condition_number":2968.1409911144938},"system_condition":{"min_eigval":-0.1156766596181279,"max_eigval":7.89068924365826,"condition_number":127635.77025046812}},{"element_name":"Hex20","epsilon_index":16,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-8.019927093786855,"max_eigval":12.25535392116576,"condition_number":42027.6209123566},"mass_condition":{"min_eigval":0.00457459783768841,"max_eigval":14.124570772489735,"condition_number":3087.609287995253},"system_condition":{"min_eigval":-7.912378536746183,"max_eigval":12.367557872325625,"condition_number":12842.015043139087}},{"element_name":"Tet4","epsilon_index":24,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-65373.23537111434,"max_eigval":26010993.00701929,"condition_number":8.365337764512934e17},"mass_condition":{"min_eigval":-3.0906512588170893e-15,"max_eigval":25.912610483151113,"condition_number":2.060762048568029e38},"system_condition":{"min_eigval":-10.299060950100786,"max_eigval":7236.22733208978,"condition_number":2.2674937632342505e18}},{"element_name":"Tet4","epsilon_index":24,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.02985225933280983,"max_eigval":4.500018263158139,"condition_number":424.61148502060666},"mass_condition":{"min_eigval":0.5169585704689859,"max_eigval":2.4106830767988363,"condition_number":4.663203619222059},"system_condition":{"min_eigval":-0.01817508849107563,"max_eigval":4.474553674886668,"condition_number":1919.863009318163}},{"element_name":"Tet4","epsilon_index":24,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.1763456684094411,"max_eigval":29.148509725050655,"condition_number":546199647896.5803},"mass_condition":{"min_eigval":-2.796937522364483e-15,"max_eigval":22.36229479168083,"condition_number":4.95409957021983e24},"system_condition":{"min_eigval":-0.07659820915300675,"max_eigval":29.041678878409314,"condition_number":545240696734.56244}},{"element_name":"Tet10","epsilon_index":24,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-109203.74889639384,"max_eigval":22523112.92330953,"condition_number":1.9266910777219885e38},"mass_condition":{"min_eigval":-2.8484603221728492e-15,"max_eigval":8.60507502300265,"condition_number":2.0627229793552407e52},"system_condition":{"min_eigval":-29.17199554628318,"max_eigval":6259.177364297855,"condition_number":1.669262843755147e38}},{"element_name":"Tet10","epsilon_index":24,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.06878297635662496,"max_eigval":7.1632511740519735,"condition_number":1.4452223910811072e17},"mass_condition":{"min_eigval":-2.9170390563323986e-15,"max_eigval":4.261717902618846,"condition_number":1.8085886177437763e18},"system_condition":{"min_eigval":-0.06622177386020267,"max_eigval":7.163251174051976,"condition_number":2.5650978426554266e17}},{"element_name":"Tet10","epsilon_index":24,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.3282750484042625,"max_eigval":20.645003680375808,"condition_number":2.6373786086633045e26},"mass_condition":{"min_eigval":-1.7257381365710154e-15,"max_eigval":19.357537037293454,"condition_number":1.1794196715589495e40},"system_condition":{"min_eigval":-0.3116053806929789,"max_eigval":20.639374614274995,"condition_number":1.9772463917231657e26}},{"element_name":"Hex8","epsilon_index":24,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-109945.75080942802,"max_eigval":10321214.510207662,"condition_number":1.3738678811220795e42},"mass_condition":{"min_eigval":-1.4387891331768166e-15,"max_eigval":24.93003394389482,"condition_number":1.1393826251613847e43},"system_condition":{"min_eigval":-23.920713965830902,"max_eigval":2877.2925908803045,"condition_number":1.3616193300149287e42}},{"element_name":"Hex8","epsilon_index":24,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.11686812266149291,"max_eigval":5.460943053393362,"condition_number":12064.563763513446},"mass_condition":{"min_eigval":0.22402019252536035,"max_eigval":3.1880467328636266,"condition_number":14.23106862343546},"system_condition":{"min_eigval":-0.09239084359854001,"max_eigval":5.411534454925524,"condition_number":352.5558896781792}},{"element_name":"Hex8","epsilon_index":24,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.9549229577561444,"max_eigval":13.21261473416414,"condition_number":2.7243235515576955e18},"mass_condition":{"min_eigval":-5.075652276051128e-16,"max_eigval":9.61815858407019,"condition_number":1.1886123239323645e31},"system_condition":{"min_eigval":-0.665896353731138,"max_eigval":13.125800645632095,"condition_number":null}},{"element_name":"Hex20","epsilon_index":24,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-174425.86975068809,"max_eigval":33511304.745335065,"condition_number":1.6145654768127278e43},"mass_condition":{"min_eigval":-8.011493555296383e-16,"max_eigval":67.11757338390825,"condition_number":2.2757372725810843e57},"system_condition":{"min_eigval":-47.05214141335205,"max_eigval":9358.35037430894,"condition_number":7.0750731107939076e44}},{"element_name":"Hex20","epsilon_index":24,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.11988561868634408,"max_eigval":8.416333386078824,"condition_number":1.6842982525077162e17},"mass_condition":{"min_eigval":-1.850714027722634e-15,"max_eigval":11.017804286946808,"condition_number":1.9155297510765786e17},"system_condition":{"min_eigval":-0.11587464818189078,"max_eigval":8.416333386078854,"condition_number":1.3691514528615314e17}},{"element_name":"Hex20","epsilon_index":24,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-5.900883279025921,"max_eigval":12.251822485057225,"condition_number":1.0703960326073686e31},"mass_condition":{"min_eigval":-8.183008006579861e-16,"max_eigval":14.158066853386586,"condition_number":8.395796095051112e43},"system_condition":{"min_eigval":-6.800906079083017,"max_eigval":12.285175640612797,"condition_number":7.31126031722313e30}},{"element_name":"Tet4","epsilon_index":24,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-65372.220200473734,"max_eigval":26010993.85836464,"condition_number":433484642.9057245},"mass_condition":{"min_eigval":1.0380089767706615e-6,"max_eigval":25.91261171409996,"condition_number":24963764.566580538},"system_condition":{"min_eigval":-10.298829556894262,"max_eigval":7236.227568628616,"condition_number":299416573.70287454}},{"element_name":"Tet4","epsilon_index":24,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.029852260791959684,"max_eigval":4.500018220647751,"condition_number":424.61105212605264},"mass_condition":{"min_eigval":0.5169585808246235,"max_eigval":2.410683092556552,"condition_number":4.6632035562910366},"system_condition":{"min_eigval":-0.018175101966634744,"max_eigval":4.474553632713906,"condition_number":1919.8583840174374}},{"element_name":"Tet4","epsilon_index":24,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.17634613536704835,"max_eigval":29.148493610578754,"condition_number":1127.206918080475},"mass_condition":{"min_eigval":0.5918864712971382,"max_eigval":22.362294646637668,"condition_number":37.781391755128276},"system_condition":{"min_eigval":-0.07659796907792216,"max_eigval":29.041663006298837,"condition_number":4843.415832351481}},{"element_name":"Tet10","epsilon_index":24,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-109202.47361057003,"max_eigval":22523113.057061777,"condition_number":15221771770.321585},"mass_condition":{"min_eigval":1.3919619201705168e-7,"max_eigval":8.60507517160185,"condition_number":61819759.91518302},"system_condition":{"min_eigval":-29.17163725664804,"max_eigval":6259.177401469537,"condition_number":124786854151.97913}},{"element_name":"Tet10","epsilon_index":24,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.06878293964196325,"max_eigval":5.4995371344239725,"condition_number":6376.281810518792},"mass_condition":{"min_eigval":0.2556070263710521,"max_eigval":4.2617178085027545,"condition_number":16.67292902314911},"system_condition":{"min_eigval":-0.06622173326412936,"max_eigval":5.491276826756387,"condition_number":53221.6989373102}},{"element_name":"Tet10","epsilon_index":24,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-1.0596065242937183,"max_eigval":20.644998060004102,"condition_number":6913.14270213089},"mass_condition":{"min_eigval":0.2759078699281795,"max_eigval":19.357536308273676,"condition_number":70.15942065484599},"system_condition":{"min_eigval":-0.994043141375514,"max_eigval":20.63936900972174,"condition_number":24457.796016920835}},{"element_name":"Hex8","epsilon_index":24,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-109943.64804487117,"max_eigval":10321214.859231463,"condition_number":1281356012.3025546},"mass_condition":{"min_eigval":-3.661469887695363e-17,"max_eigval":24.930035243743678,"condition_number":6.808750586075521e17},"system_condition":{"min_eigval":-23.920133549587796,"max_eigval":2877.2926882458,"condition_number":1688582304.4606981}},{"element_name":"Hex8","epsilon_index":24,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.11686771516627954,"max_eigval":5.46094037566818,"condition_number":12004.5229210253},"mass_condition":{"min_eigval":2.7834539441038343e-15,"max_eigval":3.188050622986446,"condition_number":1145357777426016.0},"system_condition":{"min_eigval":-0.09239035179758724,"max_eigval":5.411532205778553,"condition_number":620.3175162310876}},{"element_name":"Hex8","epsilon_index":24,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.9549100213185895,"max_eigval":13.212599626026751,"condition_number":9220.686724206562},"mass_condition":{"min_eigval":4.6906922790412864e-14,"max_eigval":9.618157369136654,"condition_number":205047715709513.0},"system_condition":{"min_eigval":-0.6658836146112026,"max_eigval":13.125785809672433,"condition_number":282.3291748104215}},{"element_name":"Hex20","epsilon_index":24,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-174423.5147456951,"max_eigval":33511306.038451277,"condition_number":50394675362.917076},"mass_condition":{"min_eigval":3.156107573667951e-8,"max_eigval":67.11757646969026,"condition_number":2126593435.2068949},"system_condition":{"min_eigval":-47.05148253154439,"max_eigval":9358.350736210208,"condition_number":59525903709.35337}},{"element_name":"Hex20","epsilon_index":24,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.1198836576374019,"max_eigval":7.890173216014187,"condition_number":64077.54302550341},"mass_condition":{"min_eigval":0.002832109465015405,"max_eigval":11.017796481540818,"condition_number":3890.31448736071},"system_condition":{"min_eigval":-0.1158726807929525,"max_eigval":7.894192797643156,"condition_number":40939.39599952205}},{"element_name":"Hex20","epsilon_index":24,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-6.702587704837511,"max_eigval":12.251817922987033,"condition_number":42187.00263836008},"mass_condition":{"min_eigval":0.0036959060428680877,"max_eigval":14.158060900857759,"condition_number":3830.741565570443},"system_condition":{"min_eigval":-6.802844220565108,"max_eigval":12.285251189439713,"condition_number":7009.235056544088}},{"element_name":"Tet4","epsilon_index":30,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-65373.28593259686,"max_eigval":26010992.92502115,"condition_number":7.084818206797671e19},"mass_condition":{"min_eigval":-2.7568183303433336e-15,"max_eigval":25.91261028429424,"condition_number":6.625481539382461e37},"system_condition":{"min_eigval":-10.299074630753069,"max_eigval":7236.227309286776,"condition_number":8.269097626783058e19}},{"element_name":"Tet4","epsilon_index":30,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.02985225888633586,"max_eigval":4.500018227706924,"condition_number":424.6115292248415},"mass_condition":{"min_eigval":0.5169585861133901,"max_eigval":2.4106830257701106,"condition_number":4.663203379392851},"system_condition":{"min_eigval":-0.018175087277224708,"max_eigval":4.474553639843254,"condition_number":1919.8619239332238}},{"element_name":"Tet4","epsilon_index":30,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.17634562676110427,"max_eigval":29.148510194200593,"condition_number":7.751153998376021e16},"mass_condition":{"min_eigval":-2.141604916072678e-15,"max_eigval":22.362294573741416,"condition_number":1.595843529369588e34},"system_condition":{"min_eigval":-0.0765981837553112,"max_eigval":29.04167933001628,"condition_number":1.692082724598598e16}},{"element_name":"Tet10","epsilon_index":30,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-109204.04729463423,"max_eigval":22523112.897766694,"condition_number":1.7903167315279613e47},"mass_condition":{"min_eigval":-1.3467399639021078e-15,"max_eigval":8.605074990846585,"condition_number":9.073533632391531e64},"system_condition":{"min_eigval":-29.172078607501494,"max_eigval":6259.177357200333,"condition_number":1.7910400725920952e47}},{"element_name":"Tet10","epsilon_index":30,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.0687829761222191,"max_eigval":7.1632491852277544,"condition_number":1.4123346241157067e17},"mass_condition":{"min_eigval":-2.895971577738297e-15,"max_eigval":4.261717935678208,"condition_number":9.913085770275749e16},"system_condition":{"min_eigval":-0.06622177361330642,"max_eigval":7.163249185227669,"condition_number":4.025731021781991e17}},{"element_name":"Tet10","epsilon_index":30,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.32827491960078836,"max_eigval":20.645003807972408,"condition_number":1.1127298537579906e35},"mass_condition":{"min_eigval":-1.7174793767131325e-15,"max_eigval":19.357538219255826,"condition_number":1.9523653051019984e52},"system_condition":{"min_eigval":-0.31160525480064294,"max_eigval":20.639374741876395,"condition_number":1.0493057202150934e35}},{"element_name":"Hex8","epsilon_index":30,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-109946.85338460634,"max_eigval":10321214.177068794,"condition_number":1.0199149550094542e31},"mass_condition":{"min_eigval":-1.4894319171832492e-15,"max_eigval":24.930032404661052,"condition_number":7.829773380271375e53},"system_condition":{"min_eigval":-23.921022998205434,"max_eigval":2877.2924965516327,"condition_number":2.2934669729795238e30}},{"element_name":"Hex8","epsilon_index":30,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.11686833735854452,"max_eigval":5.460949306665437,"condition_number":12064.07006122125},"mass_condition":{"min_eigval":0.22402046946364554,"max_eigval":3.1880506675959532,"condition_number":14.231068594887113},"system_condition":{"min_eigval":-0.09239103618149144,"max_eigval":5.411540702570487,"condition_number":352.5556687609566}},{"element_name":"Hex8","epsilon_index":30,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.9549270226741557,"max_eigval":13.212622014353839,"condition_number":4.1755117007251405e17},"mass_condition":{"min_eigval":-2.9278271741988555e-16,"max_eigval":9.61817869996895,"condition_number":4.173514366378605e41},"system_condition":{"min_eigval":-0.6658990667916432,"max_eigval":13.125808049354143,"condition_number":6.929923343437292e17}},{"element_name":"Hex20","epsilon_index":30,"stabilization_factor":0.0,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-174433.5353664962,"max_eigval":33511298.11450524,"condition_number":9.978969219587948e51},"mass_condition":{"min_eigval":-1.1534898893405085e-15,"max_eigval":67.1175477531163,"condition_number":1.4263034915802198e66},"system_condition":{"min_eigval":-47.05429995539568,"max_eigval":9358.3485114883,"condition_number":1.6431769161487667e52}},{"element_name":"Hex20","epsilon_index":30,"stabilization_factor":0.0,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.11988634993851367,"max_eigval":8.416331156967392,"condition_number":1.3150616739393752e17},"mass_condition":{"min_eigval":-1.6653559123407549e-15,"max_eigval":11.017862656590765,"condition_number":4.469966305938403e32},"system_condition":{"min_eigval":-0.11587539379103179,"max_eigval":8.416331156967392,"condition_number":2.331679523467623e17}},{"element_name":"Hex20","epsilon_index":30,"stabilization_factor":0.0,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-5.897788771212651,"max_eigval":12.251815977417357,"condition_number":3.753663728331301e36},"mass_condition":{"min_eigval":-4.3212427010427985e-16,"max_eigval":14.158197792022246,"condition_number":8.8517420393783405e43},"system_condition":{"min_eigval":-6.79553610575139,"max_eigval":12.284945411317203,"condition_number":1.0644517609425897e35}},{"element_name":"Tet4","epsilon_index":30,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-65372.27075116818,"max_eigval":26010993.776366267,"condition_number":492056334.10146445},"mass_condition":{"min_eigval":1.0380089768386317e-6,"max_eigval":25.912611515242983,"condition_number":24963764.373370487},"system_condition":{"min_eigval":-10.29884323386294,"max_eigval":7236.227545825574,"condition_number":324151057.4035108}},{"element_name":"Tet4","epsilon_index":30,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.02985226197633636,"max_eigval":4.500018221985635,"condition_number":424.6110573427763},"mass_condition":{"min_eigval":0.5169585673934476,"max_eigval":2.41068313125798,"condition_number":4.663203752310103},"system_condition":{"min_eigval":-0.0181751024328323,"max_eigval":4.474553634424604,"condition_number":1919.8578713503298}},{"element_name":"Tet4","epsilon_index":30,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.17634612619733056,"max_eigval":29.14849444273463,"condition_number":1127.2070049421563},"mass_condition":{"min_eigval":0.5918864640767101,"max_eigval":22.36229442869871,"condition_number":37.781391847813055},"system_condition":{"min_eigval":-0.07659795224443006,"max_eigval":29.041663818710006,"condition_number":4843.414877848349}},{"element_name":"Tet10","epsilon_index":30,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-109202.77191935992,"max_eigval":22523113.031519033,"condition_number":178304640997.06467},"mass_condition":{"min_eigval":1.3919619185608638e-7,"max_eigval":8.605075139445844,"condition_number":61819759.75565876},"system_condition":{"min_eigval":-29.171720295608747,"max_eigval":6259.177394371989,"condition_number":15139094460.072073}},{"element_name":"Tet10","epsilon_index":30,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.06878294408750034,"max_eigval":5.4995372499902,"condition_number":72089.03824301099},"mass_condition":{"min_eigval":0.2556070065776228,"max_eigval":4.261718034627583,"condition_number":16.672931198908206},"system_condition":{"min_eigval":-0.06622173839008784,"max_eigval":5.491276942226881,"condition_number":8003.40958786464}},{"element_name":"Tet10","epsilon_index":30,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-1.064424122152491,"max_eigval":20.644998584711978,"condition_number":32349.692592464475},"mass_condition":{"min_eigval":0.2759078616252348,"max_eigval":19.3575374902326,"condition_number":70.15942705005598},"system_condition":{"min_eigval":-0.9987444002160264,"max_eigval":20.6393695339704,"condition_number":17621.908724932153}},{"element_name":"Hex8","epsilon_index":30,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-109944.75052877673,"max_eigval":10321214.526092142,"condition_number":13031959421.455301},"mass_condition":{"min_eigval":-3.4344573643879e-16,"max_eigval":24.930033704508208,"condition_number":1.594714305459299e17},"system_condition":{"min_eigval":-23.92044255773365,"max_eigval":2877.292593917025,"condition_number":1051912964.5469846}},{"element_name":"Hex8","epsilon_index":30,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.11686797457023927,"max_eigval":5.54557729295557,"condition_number":12138.447769031607},"mass_condition":{"min_eigval":-1.2960373511546292e-15,"max_eigval":3.1880549077384663,"condition_number":9.76314703320757e16},"system_condition":{"min_eigval":-0.09239059883641126,"max_eigval":5.500167912594717,"condition_number":400.6363299425164}},{"element_name":"Hex8","epsilon_index":30,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-0.9549636910669971,"max_eigval":13.212609633614637,"condition_number":9175.081690664618},"mass_condition":{"min_eigval":-7.244590264188606e-15,"max_eigval":9.618177484978819,"condition_number":1.5077873524271082e16},"system_condition":{"min_eigval":-0.665894916333657,"max_eigval":13.125795913162861,"condition_number":282.1482834418185}},{"element_name":"Hex20","epsilon_index":30,"stabilization_factor":1e-6,"preconditioner_name":null,"stiffness_condition":{"min_eigval":-174431.17701103125,"max_eigval":33511299.407613732,"condition_number":11749011971.101606},"mass_condition":{"min_eigval":3.15610756753598e-8,"max_eigval":67.11755083887796,"condition_number":2126592627.236645},"system_condition":{"min_eigval":-47.05364009775849,"max_eigval":9358.348873387413,"condition_number":18157779223.88557}},{"element_name":"Hex20","epsilon_index":30,"stabilization_factor":1e-6,"preconditioner_name":"Diagonal","stiffness_condition":{"min_eigval":-0.11988533374184002,"max_eigval":7.890196143870923,"condition_number":63763.50760905401},"mass_condition":{"min_eigval":0.0028271375396148202,"max_eigval":11.017863238424896,"condition_number":3897.179774255345},"system_condition":{"min_eigval":-0.11587437987461705,"max_eigval":7.894215918014767,"condition_number":11068.805538829665}},{"element_name":"Hex20","epsilon_index":30,"stabilization_factor":1e-6,"preconditioner_name":"Schwarz","stiffness_condition":{"min_eigval":-34.46045237617027,"max_eigval":36.41452894454356,"condition_number":124930.57320176027},"mass_condition":{"min_eigval":0.0036907664088119806,"max_eigval":14.158191838901653,"condition_number":3836.1116014001623},"system_condition":{"min_eigval":-17.774363868387507,"max_eigval":20.057865264363848,"condition_number":11439.818931941578}}] \ No newline at end of file diff --git a/notebooks/convergence_rate/convergence_rate.ipynb b/notebooks/convergence_rate/convergence_rate.ipynb new file mode 100644 index 0000000..5ef35a0 --- /dev/null +++ b/notebooks/convergence_rate/convergence_rate.ipynb @@ -0,0 +1,300 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib as mpl\n", + "from matplotlib import rc\n", + "from matplotlib.ticker import LogLocator\n", + "from mpl_toolkits.axes_grid1.inset_locator import inset_axes\n", + "\n", + "FOR_PRINT = True\n", + "\n", + "if FOR_PRINT:\n", + " LINE_WIDTH = 1\n", + " MARKER_SIZE = 3\n", + " FONT_SIZE = 8\n", + "\n", + " AXES_WIDTH = 0.65 * LINE_WIDTH\n", + "\n", + " plt.rcParams['grid.linewidth']=AXES_WIDTH\n", + " plt.rcParams['axes.linewidth']=AXES_WIDTH\n", + " plt.rcParams['axes.labelpad']=3.0\n", + "\n", + " plt.rcParams['xtick.major.pad']=0\n", + " plt.rcParams['xtick.major.size']=2.0\n", + " plt.rcParams['xtick.major.width']=AXES_WIDTH\n", + " plt.rcParams['xtick.minor.size']=1.0\n", + " plt.rcParams['xtick.minor.width']=0.75 * AXES_WIDTH\n", + "\n", + " plt.rcParams['ytick.major.pad']=-1.5\n", + " plt.rcParams['ytick.major.size']=2.0\n", + " plt.rcParams['ytick.major.width']=AXES_WIDTH\n", + " plt.rcParams['ytick.minor.size']=1.0\n", + " plt.rcParams['ytick.minor.width']=0.75 * AXES_WIDTH\n", + "else:\n", + " LINE_WIDTH = 6\n", + " MARKER_SIZE = 14\n", + " FONT_SIZE = 45\n", + "\n", + "%matplotlib inline\n", + "#plt.rcParams['figure.figsize'] = [15, 15]\n", + "plt.rcParams['lines.linewidth'] = LINE_WIDTH\n", + "plt.rcParams['lines.markeredgewidth'] = 0.75 * LINE_WIDTH\n", + "plt.rcParams['lines.markersize'] = MARKER_SIZE\n", + "plt.rcParams['font.size'] = FONT_SIZE\n", + "rc('text', usetex=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = list()\n", + "with open(\"hex_results.json\") as results_json_file:\n", + " data = json.load(results_json_file)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def draw_convergence_triangle(fig, ax, origin, width_inches, slope, inverted=False, color=None, polygon_kwargs=None, label=True, labelcolor=None, label_kwargs=None, zorder=None):\n", + " \"\"\"\n", + " This function draws slopes or \"convergence triangles\" into loglog plots.\n", + "\n", + " @param fig: The figure\n", + " @param ax: The axes object to draw to\n", + " @param origin: The 2D origin (usually lower-left corner) coordinate of the triangle\n", + " @param width_inches: The width in inches of the triangle\n", + " @param slope: The slope of the triangle, i.e. order of convergence\n", + " @param inverted: Whether to mirror the triangle around the origin, i.e. whether \n", + " it indicates the slope towards the lower left instead of upper right (defaults to false)\n", + " @param color: The color of the of the triangle edges (defaults to default color)\n", + " @param polygon_kwargs: Additional kwargs to the Polygon draw call that creates the slope\n", + " @param label: Whether to enable labeling the slope (defaults to true)\n", + " @param labelcolor: The color of the slope labels (defaults to the edge color)\n", + " @param label_kwargs: Additional kwargs to the Annotation draw call that creates the labels\n", + " @param zorder: The z-order value of the triangle and labels, defaults to a high value\n", + " \"\"\"\n", + "\n", + " if polygon_kwargs is None:\n", + " polygon_kwargs = {}\n", + " if label_kwargs is None:\n", + " label_kwargs = {}\n", + "\n", + " if color is not None:\n", + " polygon_kwargs[\"color\"] = color\n", + " if \"linewidth\" not in polygon_kwargs:\n", + " polygon_kwargs[\"linewidth\"] = 0.75 * mpl.rcParams[\"lines.linewidth\"]\n", + " if labelcolor is not None:\n", + " label_kwargs[\"color\"] = labelcolor\n", + " if \"color\" not in label_kwargs:\n", + " label_kwargs[\"color\"] = polygon_kwargs[\"color\"]\n", + " if \"fontsize\" not in label_kwargs:\n", + " label_kwargs[\"fontsize\"] = 0.75 * mpl.rcParams[\"font.size\"]\n", + "\n", + " if inverted:\n", + " width_inches = -width_inches\n", + " if zorder is None:\n", + " zorder = 10\n", + "\n", + " # For more information on coordinate transformations in Matplotlib see\n", + " # https://matplotlib.org/3.1.1/tutorials/advanced/transforms_tutorial.html\n", + "\n", + " # Convert the origin into figure coordinates in inches\n", + " origin_disp = ax.transData.transform(origin)\n", + " origin_dpi = fig.dpi_scale_trans.inverted().transform(origin_disp)\n", + "\n", + " # Obtain the bottom-right corner in data coordinates\n", + " corner_dpi = origin_dpi + width_inches * np.array([1.0, 0.0])\n", + " corner_disp = fig.dpi_scale_trans.transform(corner_dpi)\n", + " corner = ax.transData.inverted().transform(corner_disp)\n", + "\n", + " (x1, y1) = (origin[0], origin[1])\n", + " x2 = corner[0]\n", + "\n", + " # The width of the triangle in data coordinates\n", + " width = x2 - x1\n", + " # Compute offset of the slope\n", + " log_offset = y1 / (x1 ** slope)\n", + "\n", + " y2 = log_offset * ((x1 + width) ** slope)\n", + " height = y2 - y1\n", + "\n", + " # The vertices of the slope\n", + " a = origin\n", + " b = corner\n", + " c = [x2, y2]\n", + "\n", + " # Draw the slope triangle\n", + " X = np.array([a, b, c])\n", + " triangle = plt.Polygon(X[:3,:], fill=False, zorder=zorder, **polygon_kwargs)\n", + " ax.add_patch(triangle)\n", + "\n", + " # Convert vertices into display space\n", + " a_disp = ax.transData.transform(a)\n", + " b_disp = ax.transData.transform(b)\n", + " c_disp = ax.transData.transform(c)\n", + "\n", + " # Figure out the center of the triangle sides in display space\n", + " bottom_center_disp = a_disp + 0.5 * (b_disp - a_disp)\n", + " bottom_center = ax.transData.inverted().transform(bottom_center_disp)\n", + "\n", + " right_center_disp = b_disp + 0.5 * (c_disp - b_disp)\n", + " right_center = ax.transData.inverted().transform(right_center_disp)\n", + "\n", + " # Label alignment depending on inversion parameter\n", + " va_xlabel = \"top\" if not inverted else \"bottom\"\n", + " ha_ylabel = \"left\" if not inverted else \"right\"\n", + "\n", + " # Label offset depending on inversion parameter\n", + " offset_xlabel = [0.0, -0.33 * label_kwargs[\"fontsize\"]] if not inverted else [0.0, 0.33 * label_kwargs[\"fontsize\"]]\n", + " offset_ylabel = [0.33 * label_kwargs[\"fontsize\"], 0.0] if not inverted else [-0.33 * label_kwargs[\"fontsize\"], 0.0]\n", + "\n", + " # Draw the slope labels\n", + " ax.annotate(\"$1$\", bottom_center, xytext=offset_xlabel, textcoords='offset points', ha=\"center\", va=va_xlabel, zorder=zorder, **label_kwargs)\n", + " ax.annotate(f\"${slope}$\", right_center, xytext=offset_ylabel, textcoords='offset points', ha=ha_ylabel, va=\"center\", zorder=zorder, **label_kwargs)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "FIG_WIDTH = 3.6 if FOR_PRINT else 20\n", + "FIG_HEIGHT = 2.25 if FOR_PRINT else 12\n", + "SLOPE_WIDTH = 0.125 * FIG_WIDTH\n", + "\n", + "fig = plt.figure(figsize=(FIG_WIDTH, FIG_HEIGHT))\n", + "\n", + "def method_sort_order_key(method_name):\n", + " method_mapping = {\n", + " \"fem_hex20\": 1,\n", + " \"fcm_hex20\": 3,\n", + " \"fem_hex8\": 0,\n", + " \"fcm_hex8\": 2\n", + " }\n", + " return method_mapping[method_name]\n", + "\n", + "methods = data['methods']\n", + "method_names = sorted([method for method in methods], key=method_sort_order_key)\n", + "\n", + "\n", + "def get_label(method):\n", + " method_mapping = {\n", + " \"fem_hex20\": \"FEM Hex20\",\n", + " \"fcm_hex20\": \"FCM Hex20\",\n", + " \"fem_hex8\": \"FEM Hex8\",\n", + " \"fcm_hex8\": \"FCM Hex8\"\n", + " }\n", + " return method_mapping[method]\n", + "\n", + "for method in method_names:\n", + " method_data = methods[method]\n", + " resolutions = [entry['resolution'] for entry in method_data]\n", + " l2_errors = [entry['l2_error'] for entry in method_data]\n", + " mesh_sizes = [entry['mesh_size'] for entry in method_data]\n", + " \n", + " plt.plot(mesh_sizes, l2_errors, '-o', label=get_label(method))\n", + " \n", + "plt.legend(prop={'size': 0.75 * FONT_SIZE}, loc = 'lower right')\n", + "plt.grid()\n", + "plt.xlabel('Cell width $h$', fontsize=FONT_SIZE)\n", + "plt.ylabel('$L^2$ error', fontsize=FONT_SIZE)\n", + "plt.loglog()\n", + "\n", + "plt.xlim(2e-2, 1.5e0)\n", + "plt.ylim(1e-6, 1e-1)\n", + "\n", + "plt.axes().yaxis.set_major_locator(LogLocator(10.0, subs=(1.0,)))\n", + "plt.tick_params(axis='both', which='major', labelsize=FONT_SIZE)\n", + "\n", + "plt.grid(which='major', linestyle='-', linewidth=0.75 * LINE_WIDTH)\n", + "#plt.grid(axis='x', which='minor', linestyle='--', linewidth=0.25 * LINE_WIDTH, color=\"lightgray\")\n", + "plt.grid(which='minor', linestyle='--', linewidth=0.25 * LINE_WIDTH, color=\"lightgray\")\n", + "\n", + "# Whether to use \n", + "use_single_color_slopes = True\n", + "single_color_slopes = \"dimgrey\"\n", + "\n", + "color_per_slope = {3: \"tab:red\", 2: \"tab:green\", 1: \"tab:blue\"}\n", + "if use_single_color_slopes:\n", + " color_per_slope = {s: single_color_slopes for s,c in color_per_slope.items()}\n", + "\n", + "ax = plt.gca()\n", + "draw_convergence_triangle(fig, ax, [2.00e-1, 2.5e-4], SLOPE_WIDTH, 3, color=color_per_slope[3])\n", + "draw_convergence_triangle(fig, ax, [7.00e-2, 5.25e-4], SLOPE_WIDTH, 2, color=color_per_slope[2])\n", + "draw_convergence_triangle(fig, ax, [2.60e-1, 2.0e-2], SLOPE_WIDTH, 1, color=color_per_slope[1], inverted=True)\n", + "\n", + "plt.tight_layout()\n", + "#plt.savefig('convergence_rate.pdf', format='pdf', bbox_inches='tight')\n", + "plt.savefig('convergence_rate.pgf', format='pgf', bbox_inches='tight')\n", + "plt.show()\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/convergence_rate/convergence_rate_output.txt b/notebooks/convergence_rate/convergence_rate_output.txt new file mode 100644 index 0000000..b08dc63 --- /dev/null +++ b/notebooks/convergence_rate/convergence_rate_output.txt @@ -0,0 +1,534 @@ + Compiling fcm_convergence v0.1.0 (/home/alongva/Projects/femproto2_convergence/fcm_convergence) + Finished release [optimized] target(s) in 4.54s + Running `target/release/fcm_convergence --resolutions 1 2 4 8 16 24 32 --reference-mesh hemisphere_50_uniform_refined2.msh` +[12:24:54] Loaded embedded mesh with 10395 elements, 2301 vertices. +[12:24:56] Mesh for reference solution: 665280 elements, 3078913 nodes. +[12:25:10] Num Dirichlet nodes: 56545 +[12:25:16] Assembling stiffness matrix... +[12:28:15] Assembled stiffness matrix: 1321335081 nnz, (9236739 x 9236739) +[12:28:39] Solving with CG... +[12:29:12] Finished 100 iterations... +[12:29:38] Finished 200 iterations... +[12:30:06] Finished 300 iterations... +[12:30:33] Finished 400 iterations... +[12:30:59] Finished 500 iterations... +[12:31:25] Finished 600 iterations... +[12:31:51] Finished 700 iterations... +[12:32:16] Finished 800 iterations... +[12:32:43] Finished 900 iterations... +[12:33:10] Finished 1000 iterations... +[12:33:37] Finished 1100 iterations... +[12:34:03] Finished 1200 iterations... +[12:34:29] Finished 1300 iterations... +[12:34:54] Finished 1400 iterations... +[12:35:20] Finished 1500 iterations... +[12:35:47] Finished 1600 iterations... +[12:36:13] Finished 1700 iterations... +[12:36:39] Finished 1800 iterations... +[12:37:06] Finished 1900 iterations... +[12:37:31] Finished 2000 iterations... +[12:37:57] Finished 2100 iterations... +[12:38:22] Finished 2200 iterations... +[12:38:47] Finished 2300 iterations... +[12:39:13] Finished 2400 iterations... +[12:39:38] Finished 2500 iterations... +[12:40:04] Finished 2600 iterations... +[12:40:31] Finished 2700 iterations... +[12:40:58] Finished 2800 iterations... +[12:41:24] Finished 2900 iterations... +[12:41:52] Finished 3000 iterations... +[12:42:20] Finished 3100 iterations... +[12:42:47] Finished 3200 iterations... +[12:43:13] Finished 3300 iterations... +[12:43:39] Finished 3400 iterations... +[12:44:04] Finished 3500 iterations... +[12:44:30] Finished 3600 iterations... +[12:44:55] Finished 3700 iterations... +[12:45:22] Finished 3800 iterations... +[12:45:49] Finished 3900 iterations... +[12:46:15] Finished 4000 iterations... +[12:46:31] CG iterations: 4061 +[12:46:32] Newton iters: 1 +[12:46:33] ================================== +[12:46:33] Resolution 1... +[12:46:33] Simulating FCM Hex8... +[12:46:33] Embedding mesh... +[12:46:33] Number of background cells: 4 +[12:46:33] Creating embedded model +[12:46:33] Construct stiffness quadrature +[12:46:33] Simplifying stiffness quadrature... +[12:46:33] Finished stiffness quadrature simplification. +[12:46:33] ndofs: 54, interior cells: 0, interface cells: 4 +[12:46:33] Create interpolator... +[12:46:33] Set up error/rhs quadrature +[12:46:33] Solve... +[12:46:33] Num Dirichlet nodes: 9 +[12:46:34] Assembling stiffness matrix... +[12:46:34] Assembled stiffness matrix: 1764 nnz, (54 x 54) +[12:46:34] Factoring system... +[12:46:34] Done factoring. +[12:46:34] Newton iters: 1 +[12:46:34] Estimate L2 error... +[12:46:49] L2 error: 2.730e-2 +[12:46:49] ---------------------------------- +[12:46:49] Simulating embedded FEM Hex8... +[12:46:49] Embedding mesh... +[12:46:49] Number of background cells: 4 +[12:46:49] Solve... +[12:46:49] Num Dirichlet nodes: 9 +[12:46:49] Assembling stiffness matrix... +[12:46:49] Assembled stiffness matrix: 1764 nnz, (54 x 54) +[12:46:49] Factoring system... +[12:46:49] Done factoring. +[12:46:49] Newton iters: 1 +[12:46:49] Create interpolator... +[12:46:49] Estimate L2 error... +[12:47:04] L2 error: 3.882e-2 +[12:47:04] ---------------------------------- +[12:47:04] Simulating FCM Hex20... +[12:47:04] Embedding mesh... +[12:47:04] Number of background cells: 4 +[12:47:04] Creating embedded model +[12:47:04] Construct stiffness quadrature +[12:47:04] Simplifying stiffness quadrature... +[12:47:08] Finished stiffness quadrature simplification. +[12:47:08] ndofs: 153, interior cells: 0, interface cells: 4 +[12:47:08] Create interpolator... +[12:47:08] Set up error/rhs quadrature +[12:47:08] Solve... +[12:47:08] Num Dirichlet nodes: 21 +[12:47:08] Assembling stiffness matrix... +[12:47:08] Assembled stiffness matrix: 12177 nnz, (153 x 153) +[12:47:08] Factoring system... +[12:47:08] Done factoring. +[12:47:08] Newton iters: 1 +[12:47:08] Estimate L2 error... +[12:47:23] L2 error: 1.557e-2 +[12:47:23] ---------------------------------- +[12:47:23] Simulating embedded FEM Hex20... +[12:47:23] Embedding mesh... +[12:47:23] Number of background cells: 4 +[12:47:23] Solve... +[12:47:23] Num Dirichlet nodes: 21 +[12:47:23] Assembling stiffness matrix... +[12:47:23] Assembled stiffness matrix: 12177 nnz, (153 x 153) +[12:47:23] Factoring system... +[12:47:23] Done factoring. +[12:47:23] Newton iters: 1 +[12:47:23] Create interpolator... +[12:47:23] Estimate L2 error... +[12:47:38] L2 error: 2.800e-2 +[12:47:38] ================================== +[12:47:38] Resolution 2... +[12:47:38] Simulating FCM Hex8... +[12:47:38] Embedding mesh... +[12:47:38] Number of background cells: 32 +[12:47:38] Creating embedded model +[12:47:38] Construct stiffness quadrature +[12:47:38] Simplifying stiffness quadrature... +[12:47:38] Finished stiffness quadrature simplification. +[12:47:38] ndofs: 225, interior cells: 4, interface cells: 28 +[12:47:38] Create interpolator... +[12:47:38] Set up error/rhs quadrature +[12:47:38] Solve... +[12:47:38] Num Dirichlet nodes: 25 +[12:47:38] Assembling stiffness matrix... +[12:47:38] Assembled stiffness matrix: 10647 nnz, (225 x 225) +[12:47:38] Factoring system... +[12:47:38] Done factoring. +[12:47:38] Newton iters: 1 +[12:47:38] Estimate L2 error... +[12:47:57] L2 error: 1.505e-2 +[12:47:57] ---------------------------------- +[12:47:57] Simulating embedded FEM Hex8... +[12:47:57] Embedding mesh... +[12:47:57] Number of background cells: 32 +[12:47:57] Solve... +[12:47:57] Num Dirichlet nodes: 25 +[12:47:57] Assembling stiffness matrix... +[12:47:57] Assembled stiffness matrix: 10647 nnz, (225 x 225) +[12:47:57] Factoring system... +[12:47:57] Done factoring. +[12:47:57] Newton iters: 1 +[12:47:57] Create interpolator... +[12:47:57] Estimate L2 error... +[12:48:16] L2 error: 2.127e-2 +[12:48:16] ---------------------------------- +[12:48:16] Simulating FCM Hex20... +[12:48:16] Embedding mesh... +[12:48:16] Number of background cells: 32 +[12:48:16] Creating embedded model +[12:48:16] Construct stiffness quadrature +[12:48:17] Simplifying stiffness quadrature... +[12:48:19] Finished stiffness quadrature simplification. +[12:48:19] ndofs: 735, interior cells: 4, interface cells: 28 +[12:48:19] Create interpolator... +[12:48:19] Set up error/rhs quadrature +[12:48:19] Solve... +[12:48:19] Num Dirichlet nodes: 65 +[12:48:19] Assembling stiffness matrix... +[12:48:19] Assembled stiffness matrix: 81657 nnz, (735 x 735) +[12:48:19] Factoring system... +[12:48:19] Done factoring. +[12:48:19] Newton iters: 1 +[12:48:19] Estimate L2 error... +[12:48:39] L2 error: 6.601e-3 +[12:48:39] ---------------------------------- +[12:48:39] Simulating embedded FEM Hex20... +[12:48:39] Embedding mesh... +[12:48:39] Number of background cells: 32 +[12:48:39] Solve... +[12:48:39] Num Dirichlet nodes: 65 +[12:48:39] Assembling stiffness matrix... +[12:48:39] Assembled stiffness matrix: 81657 nnz, (735 x 735) +[12:48:39] Factoring system... +[12:48:39] Done factoring. +[12:48:39] Newton iters: 1 +[12:48:39] Create interpolator... +[12:48:39] Estimate L2 error... +[12:48:58] L2 error: 1.253e-2 +[12:48:58] ================================== +[12:48:58] Resolution 4... +[12:48:58] Simulating FCM Hex8... +[12:48:58] Embedding mesh... +[12:48:58] Number of background cells: 204 +[12:48:58] Creating embedded model +[12:48:58] Construct stiffness quadrature +[12:48:58] Simplifying stiffness quadrature... +[12:48:58] Finished stiffness quadrature simplification. +[12:48:58] ndofs: 1035, interior cells: 68, interface cells: 136 +[12:48:58] Create interpolator... +[12:48:58] Set up error/rhs quadrature +[12:48:58] Solve... +[12:48:58] Num Dirichlet nodes: 77 +[12:48:59] Assembling stiffness matrix... +[12:48:59] Assembled stiffness matrix: 59841 nnz, (1035 x 1035) +[12:48:59] Factoring system... +[12:48:59] Done factoring. +[12:48:59] Newton iters: 1 +[12:48:59] Estimate L2 error... +[12:49:36] L2 error: 8.253e-3 +[12:49:36] ---------------------------------- +[12:49:36] Simulating embedded FEM Hex8... +[12:49:36] Embedding mesh... +[12:49:36] Number of background cells: 204 +[12:49:36] Solve... +[12:49:36] Num Dirichlet nodes: 77 +[12:49:36] Assembling stiffness matrix... +[12:49:36] Assembled stiffness matrix: 59841 nnz, (1035 x 1035) +[12:49:36] Factoring system... +[12:49:36] Done factoring. +[12:49:36] Newton iters: 1 +[12:49:36] Create interpolator... +[12:49:36] Estimate L2 error... +[12:50:12] L2 error: 1.368e-2 +[12:50:12] ---------------------------------- +[12:50:12] Simulating FCM Hex20... +[12:50:12] Embedding mesh... +[12:50:12] Number of background cells: 204 +[12:50:12] Creating embedded model +[12:50:12] Construct stiffness quadrature +[12:50:12] Simplifying stiffness quadrature... +[12:50:13] Finished stiffness quadrature simplification. +[12:50:13] ndofs: 3651, interior cells: 68, interface cells: 136 +[12:50:13] Create interpolator... +[12:50:13] Set up error/rhs quadrature +[12:50:14] Solve... +[12:50:14] Num Dirichlet nodes: 213 +[12:50:14] Assembling stiffness matrix... +[12:50:14] Assembled stiffness matrix: 481833 nnz, (3651 x 3651) +[12:50:14] Factoring system... +[12:50:14] Done factoring. +[12:50:14] Newton iters: 1 +[12:50:14] Estimate L2 error... +[12:50:51] L2 error: 8.225e-4 +[12:50:51] ---------------------------------- +[12:50:51] Simulating embedded FEM Hex20... +[12:50:51] Embedding mesh... +[12:50:51] Number of background cells: 204 +[12:50:51] Solve... +[12:50:51] Num Dirichlet nodes: 213 +[12:50:51] Assembling stiffness matrix... +[12:50:51] Assembled stiffness matrix: 481833 nnz, (3651 x 3651) +[12:50:51] Factoring system... +[12:50:51] Done factoring. +[12:50:51] Newton iters: 1 +[12:50:51] Create interpolator... +[12:50:51] Estimate L2 error... +[12:51:28] L2 error: 7.409e-3 +[12:51:28] ================================== +[12:51:28] Resolution 8... +[12:51:28] Simulating FCM Hex8... +[12:51:28] Embedding mesh... +[12:51:28] Number of background cells: 1364 +[12:51:28] Creating embedded model +[12:51:28] Construct stiffness quadrature +[12:51:28] Simplifying stiffness quadrature... +[12:51:28] Finished stiffness quadrature simplification. +[12:51:28] ndofs: 5559, interior cells: 784, interface cells: 580 +[12:51:28] Create interpolator... +[12:51:28] Set up error/rhs quadrature +[12:51:28] Solve... +[12:51:28] Num Dirichlet nodes: 257 +[12:51:29] Assembling stiffness matrix... +[12:51:29] Assembled stiffness matrix: 368829 nnz, (5559 x 5559) +[12:51:29] Factoring system... +[12:51:29] Done factoring. +[12:51:29] Newton iters: 1 +[12:51:29] Estimate L2 error... +[12:52:27] L2 error: 2.434e-3 +[12:52:28] ---------------------------------- +[12:52:28] Simulating embedded FEM Hex8... +[12:52:28] Embedding mesh... +[12:52:28] Number of background cells: 1364 +[12:52:28] Solve... +[12:52:28] Num Dirichlet nodes: 257 +[12:52:28] Assembling stiffness matrix... +[12:52:28] Assembled stiffness matrix: 368829 nnz, (5559 x 5559) +[12:52:28] Factoring system... +[12:52:28] Done factoring. +[12:52:28] Newton iters: 1 +[12:52:28] Create interpolator... +[12:52:28] Estimate L2 error... +[12:53:26] L2 error: 6.470e-3 +[12:53:26] ---------------------------------- +[12:53:26] Simulating FCM Hex20... +[12:53:26] Embedding mesh... +[12:53:27] Number of background cells: 1364 +[12:53:27] Creating embedded model +[12:53:27] Construct stiffness quadrature +[12:53:27] Simplifying stiffness quadrature... +[12:53:28] Finished stiffness quadrature simplification. +[12:53:28] ndofs: 20643, interior cells: 784, interface cells: 580 +[12:53:28] Create interpolator... +[12:53:28] Set up error/rhs quadrature +[12:53:28] Solve... +[12:53:28] Num Dirichlet nodes: 737 +[12:53:29] Assembling stiffness matrix... +[12:53:29] Assembled stiffness matrix: 3064977 nnz, (20643 x 20643) +[12:53:29] Factoring system... +[12:53:29] Done factoring. +[12:53:29] Newton iters: 1 +[12:53:29] Estimate L2 error... +[12:54:30] L2 error: 1.078e-4 +[12:54:30] ---------------------------------- +[12:54:30] Simulating embedded FEM Hex20... +[12:54:30] Embedding mesh... +[12:54:30] Number of background cells: 1364 +[12:54:30] Solve... +[12:54:30] Num Dirichlet nodes: 737 +[12:54:30] Assembling stiffness matrix... +[12:54:30] Assembled stiffness matrix: 3064977 nnz, (20643 x 20643) +[12:54:30] Factoring system... +[12:54:31] Done factoring. +[12:54:31] Newton iters: 1 +[12:54:31] Create interpolator... +[12:54:31] Estimate L2 error... +[12:55:30] L2 error: 4.935e-3 +[12:55:30] ================================== +[12:55:30] Resolution 16... +[12:55:30] Simulating FCM Hex8... +[12:55:30] Embedding mesh... +[12:55:30] Number of background cells: 9772 +[12:55:30] Creating embedded model +[12:55:30] Construct stiffness quadrature +[12:55:30] Simplifying stiffness quadrature... +[12:55:30] Finished stiffness quadrature simplification. +[12:55:30] ndofs: 34695, interior cells: 7392, interface cells: 2380 +[12:55:30] Create interpolator... +[12:55:30] Set up error/rhs quadrature +[12:55:30] Solve... +[12:55:30] Num Dirichlet nodes: 921 +[12:55:31] Assembling stiffness matrix... +[12:55:31] Assembled stiffness matrix: 2515437 nnz, (34695 x 34695) +[12:55:31] Factoring system... +[12:55:32] Done factoring. +[12:55:32] Newton iters: 1 +[12:55:32] Estimate L2 error... +[12:57:29] L2 error: 6.554e-4 +[12:57:29] ---------------------------------- +[12:57:29] Simulating embedded FEM Hex8... +[12:57:29] Embedding mesh... +[12:57:29] Number of background cells: 9763 +[12:57:29] Solve... +[12:57:29] Num Dirichlet nodes: 921 +[12:57:29] Assembling stiffness matrix... +[12:57:29] Assembled stiffness matrix: 2513250 nnz, (34668 x 34668) +[12:57:29] Factoring system... +[12:57:30] Done factoring. +[12:57:30] Newton iters: 1 +[12:57:30] Create interpolator... +[12:57:30] Estimate L2 error... +[12:59:23] L2 error: 3.566e-3 +[12:59:23] ---------------------------------- +[12:59:23] Simulating FCM Hex20... +[12:59:23] Embedding mesh... +[12:59:23] Number of background cells: 9772 +[12:59:23] Creating embedded model +[12:59:23] Construct stiffness quadrature +[12:59:24] Simplifying stiffness quadrature... +[12:59:25] Finished stiffness quadrature simplification. +[12:59:26] ndofs: 133155, interior cells: 7392, interface cells: 2380 +[12:59:26] Create interpolator... +[12:59:26] Set up error/rhs quadrature +[12:59:26] Solve... +[12:59:26] Num Dirichlet nodes: 2697 +[12:59:27] Assembling stiffness matrix... +[12:59:29] Assembled stiffness matrix: 21309777 nnz, (133155 x 133155) +[12:59:29] Factoring system... +[12:59:33] Done factoring. +[12:59:33] Newton iters: 1 +[12:59:33] Estimate L2 error... +[13:01:37] L2 error: 1.621e-5 +[13:01:37] ---------------------------------- +[13:01:37] Simulating embedded FEM Hex20... +[13:01:37] Embedding mesh... +[13:01:37] Number of background cells: 9763 +[13:01:37] Solve... +[13:01:37] Num Dirichlet nodes: 2697 +[13:01:37] Assembling stiffness matrix... +[13:01:39] Assembled stiffness matrix: 21290823 nnz, (133047 x 133047) +[13:01:39] Factoring system... +[13:01:43] Done factoring. +[13:01:43] Newton iters: 1 +[13:01:43] Create interpolator... +[13:01:44] Estimate L2 error... +[13:03:40] L2 error: 3.259e-3 +[13:03:40] ================================== +[13:03:40] Resolution 24... +[13:03:40] Simulating FCM Hex8... +[13:03:40] Embedding mesh... +[13:03:41] Number of background cells: 31624 +[13:03:41] Creating embedded model +[13:03:41] Construct stiffness quadrature +[13:03:41] Simplifying stiffness quadrature... +[13:03:41] Finished stiffness quadrature simplification. +[13:03:41] ndofs: 106539, interior cells: 26260, interface cells: 5364 +[13:03:41] Create interpolator... +[13:03:42] Set up error/rhs quadrature +[13:03:42] Solve... +[13:03:42] Num Dirichlet nodes: 1981 +[13:03:43] Assembling stiffness matrix... +[13:03:44] Assembled stiffness matrix: 7993089 nnz, (106539 x 106539) +[13:03:44] Factoring system... +[13:03:46] Done factoring. +[13:03:46] Newton iters: 1 +[13:03:46] Estimate L2 error... +[13:05:41] L2 error: 2.991e-4 +[13:05:41] ---------------------------------- +[13:05:41] Simulating embedded FEM Hex8... +[13:05:41] Embedding mesh... +[13:05:41] Number of background cells: 31475 +[13:05:41] Solve... +[13:05:41] Num Dirichlet nodes: 1981 +[13:05:42] Assembling stiffness matrix... +[13:05:43] Assembled stiffness matrix: 7956882 nnz, (106092 x 106092) +[13:05:43] Factoring system... +[13:05:45] Done factoring. +[13:05:45] Newton iters: 1 +[13:05:45] Create interpolator... +[13:05:45] Estimate L2 error... +[13:07:47] L2 error: 2.607e-3 +[13:07:47] ---------------------------------- +[13:07:47] Simulating FCM Hex20... +[13:07:47] Embedding mesh... +[13:07:47] Number of background cells: 31624 +[13:07:47] Creating embedded model +[13:07:47] Construct stiffness quadrature +[13:07:47] Simplifying stiffness quadrature... +[13:07:50] Finished stiffness quadrature simplification. +[13:07:50] ndofs: 414123, interior cells: 26260, interface cells: 5364 +[13:07:50] Create interpolator... +[13:07:50] Set up error/rhs quadrature +[13:07:51] Solve... +[13:07:51] Num Dirichlet nodes: 5845 +[13:07:52] Assembling stiffness matrix... +[13:07:59] Assembled stiffness matrix: 68202657 nnz, (414123 x 414123) +[13:08:00] Factoring system... +[13:08:27] Done factoring. +[13:08:27] Newton iters: 1 +[13:08:27] Estimate L2 error... +[13:10:25] L2 error: 5.386e-6 +[13:10:25] ---------------------------------- +[13:10:25] Simulating embedded FEM Hex20... +[13:10:25] Embedding mesh... +[13:10:26] Number of background cells: 31475 +[13:10:26] Solve... +[13:10:26] Num Dirichlet nodes: 5845 +[13:10:26] Assembling stiffness matrix... +[13:10:33] Assembled stiffness matrix: 67888863 nnz, (412335 x 412335) +[13:10:33] Factoring system... +[13:10:58] Done factoring. +[13:10:59] Newton iters: 1 +[13:10:59] Create interpolator... +[13:10:59] Estimate L2 error... +[13:12:58] L2 error: 2.569e-3 +[13:12:58] ================================== +[13:12:58] Resolution 32... +[13:12:58] Simulating FCM Hex8... +[13:12:58] Embedding mesh... +[13:12:59] Number of background cells: 73259 +[13:12:59] Creating embedded model +[13:12:59] Construct stiffness quadrature +[13:12:59] Simplifying stiffness quadrature... +[13:12:59] Finished stiffness quadrature simplification. +[13:13:00] ndofs: 240252, interior cells: 63720, interface cells: 9539 +[13:13:00] Create interpolator... +[13:13:00] Set up error/rhs quadrature +[13:13:00] Solve... +[13:13:00] Num Dirichlet nodes: 3461 +[13:13:02] Assembling stiffness matrix... +[13:13:05] Assembled stiffness matrix: 18346050 nnz, (240252 x 240252) +[13:13:05] Factoring system... +[13:13:10] Done factoring. +[13:13:10] Newton iters: 1 +[13:13:10] Estimate L2 error... +[13:17:55] L2 error: 1.702e-4 +[13:17:55] ---------------------------------- +[13:17:55] Simulating embedded FEM Hex8... +[13:17:55] Embedding mesh... +[13:17:55] Number of background cells: 73064 +[13:17:56] Solve... +[13:17:56] Num Dirichlet nodes: 3453 +[13:17:56] Assembling stiffness matrix... +[13:17:58] Assembled stiffness matrix: 18297936 nnz, (239640 x 239640) +[13:17:58] Factoring system... +[13:18:05] Done factoring. +[13:18:05] Newton iters: 1 +[13:18:05] Create interpolator... +[13:18:06] Estimate L2 error... +[13:22:48] L2 error: 2.240e-3 +[13:22:48] ---------------------------------- +[13:22:48] Simulating FCM Hex20... +[13:22:48] Embedding mesh... +[13:22:49] Number of background cells: 73259 +[13:22:49] Creating embedded model +[13:22:49] Construct stiffness quadrature +[13:22:49] Simplifying stiffness quadrature... +[13:22:53] Finished stiffness quadrature simplification. +[13:22:54] ndofs: 940047, interior cells: 63720, interface cells: 9539 +[13:22:54] Create interpolator... +[13:22:54] Set up error/rhs quadrature +[13:22:55] Solve... +[13:22:55] Num Dirichlet nodes: 10253 +[13:22:57] Assembling stiffness matrix... +[13:23:12] Assembled stiffness matrix: 157113855 nnz, (940047 x 940047) +[13:23:13] Factoring system... +[13:25:38] Done factoring. +[13:25:40] Newton iters: 1 +[13:25:40] Estimate L2 error... +[13:30:31] L2 error: 2.441e-6 +[13:30:31] ---------------------------------- +[13:30:31] Simulating embedded FEM Hex20... +[13:30:31] Embedding mesh... +[13:30:32] Number of background cells: 73064 +[13:30:33] Solve... +[13:30:33] Num Dirichlet nodes: 10229 +[13:30:34] Assembling stiffness matrix... +[13:30:49] Assembled stiffness matrix: 156699378 nnz, (937626 x 937626) +[13:30:49] Factoring system... +[13:33:54] Done factoring. +[13:33:57] Newton iters: 1 +[13:33:57] Create interpolator... +[13:33:57] Estimate L2 error... +[13:38:47] L2 error: 2.250e-3 diff --git a/notebooks/convergence_rate/hex_results.json b/notebooks/convergence_rate/hex_results.json new file mode 100644 index 0000000..d8805bf --- /dev/null +++ b/notebooks/convergence_rate/hex_results.json @@ -0,0 +1,152 @@ +{ + "methods": { + "fcm_hex20": [ + { + "resolution": 1, + "mesh_size": 1.0, + "l2_error": 0.015568217362350112 + }, + { + "resolution": 2, + "mesh_size": 0.5, + "l2_error": 0.006600855273375664 + }, + { + "resolution": 4, + "mesh_size": 0.25, + "l2_error": 0.0008224766685536088 + }, + { + "resolution": 8, + "mesh_size": 0.125, + "l2_error": 0.00010777672812763918 + }, + { + "resolution": 16, + "mesh_size": 0.0625, + "l2_error": 0.000016211783973513926 + }, + { + "resolution": 24, + "mesh_size": 0.041666666666666664, + "l2_error": 5.385588663732758e-6 + }, + { + "resolution": 32, + "mesh_size": 0.03125, + "l2_error": 2.4413064065673753e-6 + } + ], + "fcm_hex8": [ + { + "resolution": 1, + "mesh_size": 1.0, + "l2_error": 0.027304043294701312 + }, + { + "resolution": 2, + "mesh_size": 0.5, + "l2_error": 0.015045633757349396 + }, + { + "resolution": 4, + "mesh_size": 0.25, + "l2_error": 0.00825306942680628 + }, + { + "resolution": 8, + "mesh_size": 0.125, + "l2_error": 0.0024339772404086937 + }, + { + "resolution": 16, + "mesh_size": 0.0625, + "l2_error": 0.0006553969625189122 + }, + { + "resolution": 24, + "mesh_size": 0.041666666666666664, + "l2_error": 0.0002991481072869819 + }, + { + "resolution": 32, + "mesh_size": 0.03125, + "l2_error": 0.0001701522535675172 + } + ], + "fem_hex20": [ + { + "resolution": 1, + "mesh_size": 1.0, + "l2_error": 0.028003418360481765 + }, + { + "resolution": 2, + "mesh_size": 0.5, + "l2_error": 0.012527029765963862 + }, + { + "resolution": 4, + "mesh_size": 0.25, + "l2_error": 0.007409369481656567 + }, + { + "resolution": 8, + "mesh_size": 0.125, + "l2_error": 0.004934755066080688 + }, + { + "resolution": 16, + "mesh_size": 0.0625, + "l2_error": 0.0032585811774176543 + }, + { + "resolution": 24, + "mesh_size": 0.041666666666666664, + "l2_error": 0.002568843221155484 + }, + { + "resolution": 32, + "mesh_size": 0.03125, + "l2_error": 0.0022498423793336476 + } + ], + "fem_hex8": [ + { + "resolution": 1, + "mesh_size": 1.0, + "l2_error": 0.038817442703279845 + }, + { + "resolution": 2, + "mesh_size": 0.5, + "l2_error": 0.021271582206626553 + }, + { + "resolution": 4, + "mesh_size": 0.25, + "l2_error": 0.013676506185528967 + }, + { + "resolution": 8, + "mesh_size": 0.125, + "l2_error": 0.00647020351067994 + }, + { + "resolution": 16, + "mesh_size": 0.0625, + "l2_error": 0.0035661597210581005 + }, + { + "resolution": 24, + "mesh_size": 0.041666666666666664, + "l2_error": 0.002606527931782387 + }, + { + "resolution": 32, + "mesh_size": 0.03125, + "l2_error": 0.0022402534392801606 + } + ] + } +} \ No newline at end of file diff --git a/notebooks/mesh_reorder.ipynb b/notebooks/mesh_reorder.ipynb new file mode 100644 index 0000000..29fa322 --- /dev/null +++ b/notebooks/mesh_reorder.ipynb @@ -0,0 +1,82 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import scipy\n", + "import scipy.io\n", + "import numpy as np\n", + "import matplotlib.pylab as plt\n", + "from scipy.sparse import csr_matrix" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "original_node_matrix = scipy.io.mmread(\"../data/cylinder_shell_fine_fem_quadratic/pattern_original_nodes.mm\")\n", + "original_element_matrix = scipy.io.mmread(\"../data/cylinder_shell_fine_fem_quadratic/pattern_original_elements.mm\")\n", + "plt.figure(figsize=(20, 20))\n", + "plt.spy(original_node_matrix)\n", + "plt.figure(figsize=(20, 20))\n", + "plt.spy(original_element_matrix)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "reordered_node_matrix = scipy.io.mmread(\"../data/cylinder_shell_fine_fem_quadratic/pattern_reordered_nodes.mm\")\n", + "reordered_element_matrix = scipy.io.mmread(\"../data/cylinder_shell_fine_fem_quadratic/pattern_reordered_elements.mm\")\n", + "plt.figure(figsize=(20, 20))\n", + "plt.spy(reordered_node_matrix)\n", + "plt.figure(figsize=(20, 20))\n", + "plt.spy(reordered_element_matrix)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/quad_plots/quad_reduc_results.json b/notebooks/quad_plots/quad_reduc_results.json new file mode 100644 index 0000000..7990722 --- /dev/null +++ b/notebooks/quad_plots/quad_reduc_results.json @@ -0,0 +1,132 @@ +[ + { + "original_strength": 1, + "reduced_strength": 1, + "original_points": 6, + "reduced_points": 4, + "exact_integral": 0.3580185082735656, + "original_integral": 0.20417013839299797, + "original_abs_error": 0.15384836988056766, + "original_rel_error": 0.4297218337187488, + "reduced_integral": 0.19141791746779674, + "reduced_abs_error": 0.1666005908057689, + "reduced_rel_error": 0.4653407210960939 + }, + { + "original_strength": 2, + "reduced_strength": 2, + "original_points": 24, + "reduced_points": 10, + "exact_integral": 0.3580185082735656, + "original_integral": 0.34388536794373625, + "original_abs_error": 0.014133140329829375, + "original_rel_error": 0.039476004740598765, + "reduced_integral": 0.3669910406936639, + "reduced_abs_error": 0.008972532420098267, + "reduced_rel_error": 0.02506164405679905 + }, + { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.3580185082735656, + "original_integral": 0.3469088822812661, + "original_abs_error": 0.011109625992299532, + "original_rel_error": 0.031030870571111795, + "reduced_integral": 0.2954676719799693, + "reduced_abs_error": 0.06255083629359631, + "reduced_rel_error": 0.17471397385355444 + }, + { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.3580185082735656, + "original_integral": 0.3474000591285993, + "original_abs_error": 0.010618449144966347, + "original_rel_error": 0.029658939131863765, + "reduced_integral": 0.3268091724753725, + "reduced_abs_error": 0.031209335798193127, + "reduced_rel_error": 0.0871724089033569 + }, + { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.3580185082735656, + "original_integral": 0.3474000591285993, + "original_abs_error": 0.010618449144966347, + "original_rel_error": 0.029658939131863765, + "reduced_integral": 0.3670838192098959, + "reduced_abs_error": 0.009065310936330284, + "reduced_rel_error": 0.025320788525836174 + }, + { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.3580185082735656, + "original_integral": 0.35801850827356546, + "original_abs_error": 1.6653345369377348e-16, + "original_rel_error": 4.651531969585314e-16, + "reduced_integral": 0.36382279317395266, + "reduced_abs_error": 0.005804284900387036, + "reduced_rel_error": 0.01621224815548341 + }, + { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.3580185082735656, + "original_integral": 0.35801850827356546, + "original_abs_error": 1.6653345369377348e-16, + "original_rel_error": 4.651531969585314e-16, + "reduced_integral": 0.3558862644080259, + "reduced_abs_error": 0.002132243865539729, + "reduced_rel_error": 0.005955680547974519 + }, + { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.3580185082735656, + "original_integral": 0.35801850827356546, + "original_abs_error": 1.6653345369377348e-16, + "original_rel_error": 4.651531969585314e-16, + "reduced_integral": 0.3606206093341824, + "reduced_abs_error": 0.002602101060616757, + "reduced_rel_error": 0.007268062964578538 + }, + { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.3580185082735656, + "original_integral": 0.35801850827356546, + "original_abs_error": 1.6653345369377348e-16, + "original_rel_error": 4.651531969585314e-16, + "reduced_integral": 0.3590001836612742, + "reduced_abs_error": 0.0009816753877085471, + "reduced_rel_error": 0.0027419682642731945 + }, + { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.3580185082735656, + "original_integral": 0.35801850827356546, + "original_abs_error": 1.6653345369377348e-16, + "original_rel_error": 4.651531969585314e-16, + "reduced_integral": 0.3580185082735364, + "reduced_abs_error": 2.9254376698872875e-14, + "reduced_rel_error": 8.171191159904868e-14 + } +] \ No newline at end of file diff --git a/notebooks/quad_plots/quad_reduc_results_monomials.json b/notebooks/quad_plots/quad_reduc_results_monomials.json new file mode 100644 index 0000000..3aa3ba1 --- /dev/null +++ b/notebooks/quad_plots/quad_reduc_results_monomials.json @@ -0,0 +1,20052 @@ +[ + { + "strength": 1, + "results": [ + { + "exponents": [ + 0, + 0, + 0 + ], + "result": { + "original_strength": 1, + "reduced_strength": 1, + "original_points": 6, + "reduced_points": 4, + "exact_integral": 0.12499999999999997, + "original_integral": 0.12500000000000003, + "original_abs_error": 5.551115123125783e-17, + "original_rel_error": 4.440892098500627e-16, + "reduced_integral": 0.12500000000000003, + "reduced_abs_error": 5.551115123125783e-17, + "reduced_rel_error": 4.440892098500627e-16 + } + }, + { + "exponents": [ + 0, + 0, + 1 + ], + "result": { + "original_strength": 1, + "reduced_strength": 1, + "original_points": 6, + "reduced_points": 4, + "exact_integral": 0.05899822973615082, + "original_integral": 0.05899822973615085, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 4.704476005425269e-16, + "reduced_integral": 0.05899822973615085, + "reduced_abs_error": 2.7755575615628914e-17, + "reduced_rel_error": 4.704476005425269e-16 + } + }, + { + "exponents": [ + 0, + 1, + 0 + ], + "result": { + "original_strength": 1, + "reduced_strength": 1, + "original_points": 6, + "reduced_points": 4, + "exact_integral": 0.057250118477433484, + "original_integral": 0.05725011847743351, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 4.8481254456389365e-16, + "reduced_integral": 0.0572501184774335, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 2.4240627228194682e-16 + } + }, + { + "exponents": [ + 1, + 0, + 0 + ], + "result": { + "original_strength": 1, + "reduced_strength": 1, + "original_points": 6, + "reduced_points": 4, + "exact_integral": 0.056360767612003086, + "original_integral": 0.05636076761200309, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 1.2311567421642888e-16, + "reduced_integral": 0.05636076761200309, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 1.2311567421642888e-16 + } + } + ] + }, + { + "strength": 2, + "results": [ + { + "exponents": [ + 0, + 0, + 0 + ], + "result": { + "original_strength": 2, + "reduced_strength": 2, + "original_points": 24, + "reduced_points": 10, + "exact_integral": 0.12499999999999997, + "original_integral": 0.125, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 2.2204460492503136e-16, + "reduced_integral": 0.12500000000000003, + "reduced_abs_error": 5.551115123125783e-17, + "reduced_rel_error": 4.440892098500627e-16 + } + }, + { + "exponents": [ + 0, + 0, + 1 + ], + "result": { + "original_strength": 2, + "reduced_strength": 2, + "original_points": 24, + "reduced_points": 10, + "exact_integral": 0.05899822973615082, + "original_integral": 0.058998229736150855, + "original_abs_error": 3.469446951953614e-17, + "original_rel_error": 5.880595006781586e-16, + "reduced_integral": 0.058998229736150834, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 2.3522380027126344e-16 + } + }, + { + "exponents": [ + 0, + 0, + 2 + ], + "result": { + "original_strength": 2, + "reduced_strength": 2, + "original_points": 24, + "reduced_points": 10, + "exact_integral": 0.030450495562663715, + "original_integral": 0.03045049556266373, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 4.557491611016812e-16, + "reduced_integral": 0.030450495562663722, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 2.278745805508406e-16 + } + }, + { + "exponents": [ + 0, + 1, + 0 + ], + "result": { + "original_strength": 2, + "reduced_strength": 2, + "original_points": 24, + "reduced_points": 10, + "exact_integral": 0.057250118477433484, + "original_integral": 0.05725011847743349, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 1.2120313614097341e-16, + "reduced_integral": 0.057250118477433505, + "reduced_abs_error": 2.0816681711721685e-17, + "reduced_rel_error": 3.6360940842292024e-16 + } + }, + { + "exponents": [ + 0, + 1, + 1 + ], + "result": { + "original_strength": 2, + "reduced_strength": 2, + "original_points": 24, + "reduced_points": 10, + "exact_integral": 0.027021245138827792, + "original_integral": 0.027021245138827803, + "original_abs_error": 1.0408340855860843e-17, + "original_rel_error": 3.8519101552817513e-16, + "reduced_integral": 0.027021245138827803, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 3.8519101552817513e-16 + } + }, + { + "exponents": [ + 0, + 2, + 0 + ], + "result": { + "original_strength": 2, + "reduced_strength": 2, + "original_points": 24, + "reduced_points": 10, + "exact_integral": 0.02882477519210804, + "original_integral": 0.02882477519210805, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 2.407267310035096e-16, + "reduced_integral": 0.02882477519210805, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 2.407267310035096e-16 + } + }, + { + "exponents": [ + 1, + 0, + 0 + ], + "result": { + "original_strength": 2, + "reduced_strength": 2, + "original_points": 24, + "reduced_points": 10, + "exact_integral": 0.056360767612003086, + "original_integral": 0.056360767612003086, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.05636076761200308, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 1.2311567421642888e-16 + } + }, + { + "exponents": [ + 1, + 0, + 1 + ], + "result": { + "original_strength": 2, + "reduced_strength": 2, + "original_points": 24, + "reduced_points": 10, + "exact_integral": 0.02660148412543013, + "original_integral": 0.02660148412543014, + "original_abs_error": 1.0408340855860843e-17, + "original_rel_error": 3.9126917907226146e-16, + "reduced_integral": 0.026601484125430133, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 1.3042305969075383e-16 + } + }, + { + "exponents": [ + 1, + 1, + 0 + ], + "result": { + "original_strength": 2, + "reduced_strength": 2, + "original_points": 24, + "reduced_points": 10, + "exact_integral": 0.02581328498613018, + "original_integral": 0.025813284986130187, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 2.6881095945888283e-16, + "reduced_integral": 0.025813284986130183, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 1.3440547972944142e-16 + } + }, + { + "exponents": [ + 2, + 0, + 0 + ], + "result": { + "original_strength": 2, + "reduced_strength": 2, + "original_points": 24, + "reduced_points": 10, + "exact_integral": 0.02801645567318039, + "original_integral": 0.028016455673180393, + "original_abs_error": 3.469446951953614e-18, + "original_rel_error": 1.2383604094770806e-16, + "reduced_integral": 0.028016455673180393, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 1.2383604094770806e-16 + } + } + ] + }, + { + "strength": 3, + "results": [ + { + "exponents": [ + 0, + 0, + 0 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.12499999999999997, + "original_integral": 0.12500000000000008, + "original_abs_error": 1.1102230246251565e-16, + "original_rel_error": 8.881784197001254e-16, + "reduced_integral": 0.125, + "reduced_abs_error": 2.7755575615628914e-17, + "reduced_rel_error": 2.2204460492503136e-16 + } + }, + { + "exponents": [ + 0, + 0, + 1 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.05899822973615082, + "original_integral": 0.05899822973615086, + "original_abs_error": 4.163336342344337e-17, + "original_rel_error": 7.056714008137903e-16, + "reduced_integral": 0.058998229736150834, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 2.3522380027126344e-16 + } + }, + { + "exponents": [ + 0, + 0, + 2 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.030450495562663715, + "original_integral": 0.03045049556266374, + "original_abs_error": 2.42861286636753e-17, + "original_rel_error": 7.975610319279422e-16, + "reduced_integral": 0.030450495562663733, + "reduced_abs_error": 1.734723475976807e-17, + "reduced_rel_error": 5.696864513771016e-16 + } + }, + { + "exponents": [ + 0, + 0, + 3 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.016830462234625018, + "original_integral": 0.016830462234625032, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 8.245636759318425e-16, + "reduced_integral": 0.016830462234625025, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 4.1228183796592125e-16 + } + }, + { + "exponents": [ + 0, + 1, + 0 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.057250118477433484, + "original_integral": 0.05725011847743349, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 1.2120313614097341e-16, + "reduced_integral": 0.05725011847743349, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 1.2120313614097341e-16 + } + }, + { + "exponents": [ + 0, + 1, + 1 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.027021245138827792, + "original_integral": 0.027021245138827806, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 5.135880207042335e-16, + "reduced_integral": 0.027021245138827803, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 3.8519101552817513e-16 + } + }, + { + "exponents": [ + 0, + 1, + 2 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.013946355829272483, + "original_integral": 0.013946355829272497, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 9.950834452886891e-16, + "reduced_integral": 0.013946355829272492, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 6.219271533054308e-16 + } + }, + { + "exponents": [ + 0, + 2, + 0 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.02882477519210804, + "original_integral": 0.028824775192108052, + "original_abs_error": 1.0408340855860843e-17, + "original_rel_error": 3.6109009650526434e-16, + "reduced_integral": 0.02882477519210805, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 2.407267310035096e-16 + } + }, + { + "exponents": [ + 0, + 2, + 1 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.013604885671015126, + "original_integral": 0.01360488567101514, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 1.0200591275368629e-15, + "reduced_integral": 0.013604885671015136, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 7.650443456526471e-16 + } + }, + { + "exponents": [ + 0, + 3, + 0 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.015587195961894966, + "original_integral": 0.015587195961894973, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 4.451662711414102e-16, + "reduced_integral": 0.015587195961894972, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 3.3387470335605764e-16 + } + }, + { + "exponents": [ + 1, + 0, + 0 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.056360767612003086, + "original_integral": 0.05636076761200311, + "original_abs_error": 2.0816681711721685e-17, + "original_rel_error": 3.6934702264928667e-16, + "reduced_integral": 0.056360767612003086, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 1, + 0, + 1 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.02660148412543013, + "original_integral": 0.026601484125430147, + "original_abs_error": 1.734723475976807e-17, + "original_rel_error": 6.521152984537691e-16, + "reduced_integral": 0.026601484125430137, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 2.6084611938150766e-16 + } + }, + { + "exponents": [ + 1, + 0, + 2 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.013729706432620965, + "original_integral": 0.013729706432620977, + "original_abs_error": 1.214306433183765e-17, + "original_rel_error": 8.844372886944219e-16, + "reduced_integral": 0.013729706432620973, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 6.317409204960156e-16 + } + }, + { + "exponents": [ + 1, + 1, + 0 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.02581328498613018, + "original_integral": 0.025813284986130183, + "original_abs_error": 3.469446951953614e-18, + "original_rel_error": 1.3440547972944142e-16, + "reduced_integral": 0.02581328498613019, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 4.032164391883242e-16 + } + }, + { + "exponents": [ + 1, + 1, + 1 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.01218350494285153, + "original_integral": 0.012183504942851538, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 5.695318331182284e-16, + "reduced_integral": 0.012183504942851536, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 4.271488748386713e-16 + } + }, + { + "exponents": [ + 1, + 2, + 0 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.012996691648565066, + "original_integral": 0.012996691648565067, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.334742350502971e-16, + "reduced_integral": 0.012996691648565066, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 2, + 0, + 0 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.02801645567318039, + "original_integral": 0.02801645567318039, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0280164556731804, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 3.7150812284312416e-16 + } + }, + { + "exponents": [ + 2, + 0, + 1 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.013223370305591857, + "original_integral": 0.013223370305591871, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 1.0494894635103625e-15, + "reduced_integral": 0.01322337030559187, + "reduced_abs_error": 1.214306433183765e-17, + "reduced_rel_error": 9.183032805715672e-16 + } + }, + { + "exponents": [ + 2, + 1, + 0 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.012831563252858731, + "original_integral": 0.012831563252858731, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.012831563252858735, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 2.703838093289732e-16 + } + }, + { + "exponents": [ + 3, + 0, + 0 + ], + "result": { + "original_strength": 3, + "reduced_strength": 3, + "original_points": 48, + "reduced_points": 20, + "exact_integral": 0.01498059689723164, + "original_integral": 0.014980596897231647, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 4.631920845016203e-16, + "reduced_integral": 0.014980596897231644, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 2.3159604225081016e-16 + } + } + ] + }, + { + "strength": 4, + "results": [ + { + "exponents": [ + 0, + 0, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.12499999999999997, + "original_integral": 0.12500000000000014, + "original_abs_error": 1.6653345369377348e-16, + "original_rel_error": 1.3322676295501882e-15, + "reduced_integral": 0.12500000000000008, + "reduced_abs_error": 1.1102230246251565e-16, + "reduced_rel_error": 8.881784197001254e-16 + } + }, + { + "exponents": [ + 0, + 0, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.05899822973615082, + "original_integral": 0.05899822973615086, + "original_abs_error": 4.163336342344337e-17, + "original_rel_error": 7.056714008137903e-16, + "reduced_integral": 0.058998229736150876, + "reduced_abs_error": 5.551115123125783e-17, + "reduced_rel_error": 9.408952010850538e-16 + } + }, + { + "exponents": [ + 0, + 0, + 2 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.030450495562663715, + "original_integral": 0.03045049556266373, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 4.557491611016812e-16, + "reduced_integral": 0.03045049556266375, + "reduced_abs_error": 3.469446951953614e-17, + "reduced_rel_error": 1.1393729027542032e-15 + } + }, + { + "exponents": [ + 0, + 0, + 3 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.016830462234625018, + "original_integral": 0.01683046223462503, + "original_abs_error": 1.0408340855860843e-17, + "original_rel_error": 6.184227569488818e-16, + "reduced_integral": 0.01683046223462504, + "reduced_abs_error": 2.0816681711721685e-17, + "reduced_rel_error": 1.2368455138977637e-15 + } + }, + { + "exponents": [ + 0, + 0, + 4 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.00978179162587195, + "original_integral": 0.009781791625871962, + "original_abs_error": 1.214306433183765e-17, + "original_rel_error": 1.2413947052112978e-15, + "reduced_integral": 0.00978179162587197, + "reduced_abs_error": 2.0816681711721685e-17, + "reduced_rel_error": 2.128105208933653e-15 + } + }, + { + "exponents": [ + 0, + 1, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.057250118477433484, + "original_integral": 0.057250118477433554, + "original_abs_error": 6.938893903907228e-17, + "original_rel_error": 1.2120313614097341e-15, + "reduced_integral": 0.057250118477433526, + "reduced_abs_error": 4.163336342344337e-17, + "reduced_rel_error": 7.272188168458405e-16 + } + }, + { + "exponents": [ + 0, + 1, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.027021245138827792, + "original_integral": 0.0270212451388278, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 2.5679401035211674e-16, + "reduced_integral": 0.027021245138827817, + "reduced_abs_error": 2.42861286636753e-17, + "reduced_rel_error": 8.987790362324086e-16 + } + }, + { + "exponents": [ + 0, + 1, + 2 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.013946355829272483, + "original_integral": 0.013946355829272495, + "original_abs_error": 1.214306433183765e-17, + "original_rel_error": 8.706980146276031e-16, + "reduced_integral": 0.0139463558292725, + "reduced_abs_error": 1.734723475976807e-17, + "reduced_rel_error": 1.2438543066108616e-15 + } + }, + { + "exponents": [ + 0, + 1, + 3 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.0077083676556980165, + "original_integral": 0.007708367655698026, + "original_abs_error": 9.540979117872439e-18, + "original_rel_error": 1.2377431311050347e-15, + "reduced_integral": 0.007708367655698028, + "reduced_abs_error": 1.1275702593849246e-17, + "reduced_rel_error": 1.4627873367604955e-15 + } + }, + { + "exponents": [ + 0, + 2, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.02882477519210804, + "original_integral": 0.028824775192108052, + "original_abs_error": 1.0408340855860843e-17, + "original_rel_error": 3.6109009650526434e-16, + "reduced_integral": 0.028824775192108056, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 4.814534620070192e-16 + } + }, + { + "exponents": [ + 0, + 2, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.013604885671015126, + "original_integral": 0.013604885671015138, + "original_abs_error": 1.214306433183765e-17, + "original_rel_error": 8.92551736594755e-16, + "reduced_integral": 0.013604885671015142, + "reduced_abs_error": 1.5612511283791264e-17, + "reduced_rel_error": 1.1475665184789707e-15 + } + }, + { + "exponents": [ + 0, + 2, + 2 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.007021829512656519, + "original_integral": 0.007021829512656526, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 9.881888888643901e-16, + "reduced_integral": 0.0070218295126565295, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 1.4822833332965853e-15 + } + }, + { + "exponents": [ + 0, + 3, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.015587195961894966, + "original_integral": 0.015587195961894972, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 3.3387470335605764e-16, + "reduced_integral": 0.015587195961894972, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 3.3387470335605764e-16 + } + }, + { + "exponents": [ + 0, + 3, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.007356935746418251, + "original_integral": 0.007356935746418259, + "original_abs_error": 7.806255641895632e-18, + "original_rel_error": 1.0610743264539362e-15, + "reduced_integral": 0.007356935746418259, + "reduced_abs_error": 7.806255641895632e-18, + "reduced_rel_error": 1.0610743264539362e-15 + } + }, + { + "exponents": [ + 0, + 4, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.008875394807235764, + "original_integral": 0.008875394807235766, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.9545310531567051e-16, + "reduced_integral": 0.008875394807235766, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 1.9545310531567051e-16 + } + }, + { + "exponents": [ + 1, + 0, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.056360767612003086, + "original_integral": 0.05636076761200307, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 2.4623134843285776e-16, + "reduced_integral": 0.05636076761200312, + "reduced_abs_error": 3.469446951953614e-17, + "reduced_rel_error": 6.155783710821444e-16 + } + }, + { + "exponents": [ + 1, + 0, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.02660148412543013, + "original_integral": 0.02660148412543015, + "original_abs_error": 2.0816681711721685e-17, + "original_rel_error": 7.825383581445229e-16, + "reduced_integral": 0.026601484125430154, + "reduced_abs_error": 2.42861286636753e-17, + "reduced_rel_error": 9.129614178352768e-16 + } + }, + { + "exponents": [ + 1, + 0, + 2 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.013729706432620965, + "original_integral": 0.013729706432620977, + "original_abs_error": 1.214306433183765e-17, + "original_rel_error": 8.844372886944219e-16, + "reduced_integral": 0.013729706432620978, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 1.0107854727936251e-15 + } + }, + { + "exponents": [ + 1, + 0, + 3 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.007588622166466357, + "original_integral": 0.007588622166466364, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 9.143812607471436e-16, + "reduced_integral": 0.0075886221664663665, + "reduced_abs_error": 9.540979117872439e-18, + "reduced_rel_error": 1.2572742335273226e-15 + } + }, + { + "exponents": [ + 1, + 1, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.02581328498613018, + "original_integral": 0.02581328498613019, + "original_abs_error": 1.0408340855860843e-17, + "original_rel_error": 4.032164391883242e-16, + "reduced_integral": 0.0258132849861302, + "reduced_abs_error": 2.0816681711721685e-17, + "reduced_rel_error": 8.064328783766484e-16 + } + }, + { + "exponents": [ + 1, + 1, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.01218350494285153, + "original_integral": 0.012183504942851534, + "original_abs_error": 3.469446951953614e-18, + "original_rel_error": 2.847659165591142e-16, + "reduced_integral": 0.012183504942851541, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 8.542977496773426e-16 + } + }, + { + "exponents": [ + 1, + 1, + 2 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.006288218559423448, + "original_integral": 0.006288218559423453, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 8.276064800787681e-16, + "reduced_integral": 0.006288218559423455, + "reduced_abs_error": 7.806255641895632e-18, + "reduced_rel_error": 1.241409720118152e-15 + } + }, + { + "exponents": [ + 1, + 2, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.012996691648565066, + "original_integral": 0.012996691648565064, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.334742350502971e-16, + "reduced_integral": 0.012996691648565073, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 5.338969402011884e-16 + } + }, + { + "exponents": [ + 1, + 2, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.006134254397535635, + "original_integral": 0.006134254397535639, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 7.069822033602453e-16, + "reduced_integral": 0.006134254397535639, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 7.069822033602453e-16 + } + }, + { + "exponents": [ + 1, + 3, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.007028050634648921, + "original_integral": 0.0070280506346489255, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 6.170713495661459e-16, + "reduced_integral": 0.007028050634648923, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.4682853982645837e-16 + } + }, + { + "exponents": [ + 2, + 0, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.02801645567318039, + "original_integral": 0.028016455673180413, + "original_abs_error": 2.42861286636753e-17, + "original_rel_error": 8.668522866339563e-16, + "reduced_integral": 0.028016455673180406, + "reduced_abs_error": 1.734723475976807e-17, + "reduced_rel_error": 6.191802047385402e-16 + } + }, + { + "exponents": [ + 2, + 0, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.013223370305591857, + "original_integral": 0.01322337030559187, + "original_abs_error": 1.214306433183765e-17, + "original_rel_error": 9.183032805715672e-16, + "reduced_integral": 0.013223370305591871, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 1.0494894635103625e-15 + } + }, + { + "exponents": [ + 2, + 0, + 2 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.006824919673261954, + "original_integral": 0.006824919673261958, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 6.35437323450468e-16, + "reduced_integral": 0.00682491967326196, + "reduced_abs_error": 6.071532165918825e-18, + "reduced_rel_error": 8.89612252830655e-16 + } + }, + { + "exponents": [ + 2, + 1, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.012831563252858731, + "original_integral": 0.012831563252858724, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 5.407676186579464e-16, + "reduced_integral": 0.012831563252858733, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 1.351919046644866e-16 + } + }, + { + "exponents": [ + 2, + 1, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.00605631613332888, + "original_integral": 0.006056316133328885, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 8.592963632283057e-16, + "reduced_integral": 0.006056316133328886, + "reduced_abs_error": 6.071532165918825e-18, + "reduced_rel_error": 1.0025124237663566e-15 + } + }, + { + "exponents": [ + 2, + 2, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.006460544291672678, + "original_integral": 0.006460544291672678, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.00646054429167268, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 4.0276563343419377e-16 + } + }, + { + "exponents": [ + 3, + 0, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.01498059689723164, + "original_integral": 0.014980596897231637, + "original_abs_error": 3.469446951953614e-18, + "original_rel_error": 2.3159604225081016e-16, + "reduced_integral": 0.014980596897231642, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 1.1579802112540508e-16 + } + }, + { + "exponents": [ + 3, + 0, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.007070629578620323, + "original_integral": 0.007070629578620329, + "original_abs_error": 6.071532165918825e-18, + "original_rel_error": 8.586975315858012e-16, + "reduced_integral": 0.007070629578620328, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 7.360264556449725e-16 + } + }, + { + "exponents": [ + 3, + 1, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.006861127577833471, + "original_integral": 0.006861127577833471, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0068611275778334745, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 5.056671680559476e-16 + } + }, + { + "exponents": [ + 4, + 0, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 4, + "original_points": 84, + "reduced_points": 35, + "exact_integral": 0.008440467836218837, + "original_integral": 0.008440467836218846, + "original_abs_error": 8.673617379884035e-18, + "original_rel_error": 1.027622822358819e-15, + "reduced_integral": 0.008440467836218844, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 8.220982578870552e-16 + } + } + ] + }, + { + "strength": 5, + "results": [ + { + "exponents": [ + 0, + 0, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.12499999999999997, + "original_integral": 0.12500000000000014, + "original_abs_error": 1.6653345369377348e-16, + "original_rel_error": 1.3322676295501882e-15, + "reduced_integral": 0.1250000000000001, + "reduced_abs_error": 1.3877787807814457e-16, + "reduced_rel_error": 1.1102230246251567e-15 + } + }, + { + "exponents": [ + 0, + 0, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.05899822973615082, + "original_integral": 0.05899822973615086, + "original_abs_error": 4.163336342344337e-17, + "original_rel_error": 7.056714008137903e-16, + "reduced_integral": 0.05899822973615087, + "reduced_abs_error": 4.85722573273506e-17, + "reduced_rel_error": 8.23283300949422e-16 + } + }, + { + "exponents": [ + 0, + 0, + 2 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.030450495562663715, + "original_integral": 0.03045049556266373, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 4.557491611016812e-16, + "reduced_integral": 0.03045049556266375, + "reduced_abs_error": 3.469446951953614e-17, + "reduced_rel_error": 1.1393729027542032e-15 + } + }, + { + "exponents": [ + 0, + 0, + 3 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.016830462234625018, + "original_integral": 0.01683046223462503, + "original_abs_error": 1.0408340855860843e-17, + "original_rel_error": 6.184227569488818e-16, + "reduced_integral": 0.016830462234625036, + "reduced_abs_error": 1.734723475976807e-17, + "reduced_rel_error": 1.0307045949148031e-15 + } + }, + { + "exponents": [ + 0, + 0, + 4 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.00978179162587195, + "original_integral": 0.009781791625871962, + "original_abs_error": 1.214306433183765e-17, + "original_rel_error": 1.2413947052112978e-15, + "reduced_integral": 0.009781791625871963, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 1.418736805955769e-15 + } + }, + { + "exponents": [ + 0, + 0, + 5 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.0058964926575299035, + "original_integral": 0.0058964926575299096, + "original_abs_error": 6.071532165918825e-18, + "original_rel_error": 1.0296853601887206e-15, + "reduced_integral": 0.005896492657529909, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 8.825874515903319e-16 + } + }, + { + "exponents": [ + 0, + 1, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.057250118477433484, + "original_integral": 0.057250118477433554, + "original_abs_error": 6.938893903907228e-17, + "original_rel_error": 1.2120313614097341e-15, + "reduced_integral": 0.05725011847743353, + "reduced_abs_error": 4.85722573273506e-17, + "reduced_rel_error": 8.484219529868139e-16 + } + }, + { + "exponents": [ + 0, + 1, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.027021245138827792, + "original_integral": 0.0270212451388278, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 2.5679401035211674e-16, + "reduced_integral": 0.027021245138827813, + "reduced_abs_error": 2.0816681711721685e-17, + "reduced_rel_error": 7.703820310563503e-16 + } + }, + { + "exponents": [ + 0, + 1, + 2 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.013946355829272483, + "original_integral": 0.013946355829272495, + "original_abs_error": 1.214306433183765e-17, + "original_rel_error": 8.706980146276031e-16, + "reduced_integral": 0.013946355829272498, + "reduced_abs_error": 1.5612511283791264e-17, + "reduced_rel_error": 1.1194688759497753e-15 + } + }, + { + "exponents": [ + 0, + 1, + 3 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.0077083676556980165, + "original_integral": 0.007708367655698026, + "original_abs_error": 9.540979117872439e-18, + "original_rel_error": 1.2377431311050347e-15, + "reduced_integral": 0.007708367655698028, + "reduced_abs_error": 1.1275702593849246e-17, + "reduced_rel_error": 1.4627873367604955e-15 + } + }, + { + "exponents": [ + 0, + 1, + 4 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.004480069836021888, + "original_integral": 0.004480069836021894, + "original_abs_error": 6.071532165918825e-18, + "original_rel_error": 1.3552315897178265e-15, + "reduced_integral": 0.004480069836021894, + "reduced_abs_error": 6.071532165918825e-18, + "reduced_rel_error": 1.3552315897178265e-15 + } + }, + { + "exponents": [ + 0, + 2, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.02882477519210804, + "original_integral": 0.028824775192108052, + "original_abs_error": 1.0408340855860843e-17, + "original_rel_error": 3.6109009650526434e-16, + "reduced_integral": 0.02882477519210806, + "reduced_abs_error": 1.734723475976807e-17, + "reduced_rel_error": 6.018168275087739e-16 + } + }, + { + "exponents": [ + 0, + 2, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.013604885671015126, + "original_integral": 0.013604885671015138, + "original_abs_error": 1.214306433183765e-17, + "original_rel_error": 8.92551736594755e-16, + "reduced_integral": 0.013604885671015136, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 7.650443456526471e-16 + } + }, + { + "exponents": [ + 0, + 2, + 2 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.007021829512656519, + "original_integral": 0.007021829512656526, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 9.881888888643901e-16, + "reduced_integral": 0.007021829512656527, + "reduced_abs_error": 7.806255641895632e-18, + "reduced_rel_error": 1.111712499972439e-15 + } + }, + { + "exponents": [ + 0, + 2, + 3 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.003881074322338644, + "original_integral": 0.003881074322338648, + "original_abs_error": 3.903127820947816e-18, + "original_rel_error": 1.005682318032983e-15, + "reduced_integral": 0.003881074322338648, + "reduced_abs_error": 3.903127820947816e-18, + "reduced_rel_error": 1.005682318032983e-15 + } + }, + { + "exponents": [ + 0, + 3, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.015587195961894966, + "original_integral": 0.015587195961894972, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 3.3387470335605764e-16, + "reduced_integral": 0.015587195961894968, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 1.1129156778535255e-16 + } + }, + { + "exponents": [ + 0, + 3, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.007356935746418251, + "original_integral": 0.007356935746418259, + "original_abs_error": 7.806255641895632e-18, + "original_rel_error": 1.0610743264539362e-15, + "reduced_integral": 0.007356935746418255, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 4.7158858953508275e-16 + } + }, + { + "exponents": [ + 0, + 3, + 2 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.0037971027317764182, + "original_integral": 0.0037971027317764234, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 1.370563504742398e-15, + "reduced_integral": 0.003797102731776422, + "reduced_abs_error": 3.903127820947816e-18, + "reduced_rel_error": 1.0279226285567986e-15 + } + }, + { + "exponents": [ + 0, + 4, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.008875394807235764, + "original_integral": 0.008875394807235766, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.9545310531567051e-16, + "reduced_integral": 0.008875394807235768, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 3.9090621063134103e-16 + } + }, + { + "exponents": [ + 0, + 4, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.004189060654690686, + "original_integral": 0.004189060654690688, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 4.141079872009866e-16, + "reduced_integral": 0.004189060654690687, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 2.070539936004933e-16 + } + }, + { + "exponents": [ + 0, + 5, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.005244601150649224, + "original_integral": 0.005244601150649227, + "original_abs_error": 3.469446951953614e-18, + "original_rel_error": 6.615273215817632e-16, + "reduced_integral": 0.0052446011506492255, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 3.307636607908816e-16 + } + }, + { + "exponents": [ + 1, + 0, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.056360767612003086, + "original_integral": 0.05636076761200307, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 2.4623134843285776e-16, + "reduced_integral": 0.05636076761200311, + "reduced_abs_error": 2.0816681711721685e-17, + "reduced_rel_error": 3.6934702264928667e-16 + } + }, + { + "exponents": [ + 1, + 0, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.02660148412543013, + "original_integral": 0.02660148412543015, + "original_abs_error": 2.0816681711721685e-17, + "original_rel_error": 7.825383581445229e-16, + "reduced_integral": 0.026601484125430154, + "reduced_abs_error": 2.42861286636753e-17, + "reduced_rel_error": 9.129614178352768e-16 + } + }, + { + "exponents": [ + 1, + 0, + 2 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.013729706432620965, + "original_integral": 0.013729706432620977, + "original_abs_error": 1.214306433183765e-17, + "original_rel_error": 8.844372886944219e-16, + "reduced_integral": 0.013729706432620977, + "reduced_abs_error": 1.214306433183765e-17, + "reduced_rel_error": 8.844372886944219e-16 + } + }, + { + "exponents": [ + 1, + 0, + 3 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.007588622166466357, + "original_integral": 0.007588622166466364, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 9.143812607471436e-16, + "reduced_integral": 0.0075886221664663665, + "reduced_abs_error": 9.540979117872439e-18, + "reduced_rel_error": 1.2572742335273226e-15 + } + }, + { + "exponents": [ + 1, + 0, + 4 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.004410474277238454, + "original_integral": 0.00441047427723846, + "original_abs_error": 6.071532165918825e-18, + "original_rel_error": 1.3766166140573016e-15, + "reduced_integral": 0.004410474277238462, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 1.9665951629390024e-15 + } + }, + { + "exponents": [ + 1, + 1, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.02581328498613018, + "original_integral": 0.02581328498613019, + "original_abs_error": 1.0408340855860843e-17, + "original_rel_error": 4.032164391883242e-16, + "reduced_integral": 0.025813284986130197, + "reduced_abs_error": 1.734723475976807e-17, + "reduced_rel_error": 6.720273986472071e-16 + } + }, + { + "exponents": [ + 1, + 1, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.01218350494285153, + "original_integral": 0.012183504942851534, + "original_abs_error": 3.469446951953614e-18, + "original_rel_error": 2.847659165591142e-16, + "reduced_integral": 0.012183504942851538, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 5.695318331182284e-16 + } + }, + { + "exponents": [ + 1, + 1, + 2 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.006288218559423448, + "original_integral": 0.006288218559423453, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 8.276064800787681e-16, + "reduced_integral": 0.006288218559423454, + "reduced_abs_error": 6.071532165918825e-18, + "reduced_rel_error": 9.655408934252295e-16 + } + }, + { + "exponents": [ + 1, + 1, + 3 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.0034755961448854167, + "original_integral": 0.0034755961448854197, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 8.73451907646622e-16, + "reduced_integral": 0.00347559614488542, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 9.982307515961395e-16 + } + }, + { + "exponents": [ + 1, + 2, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.012996691648565066, + "original_integral": 0.012996691648565064, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.334742350502971e-16, + "reduced_integral": 0.012996691648565067, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 1.334742350502971e-16 + } + }, + { + "exponents": [ + 1, + 2, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.006134254397535635, + "original_integral": 0.006134254397535639, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 7.069822033602453e-16, + "reduced_integral": 0.0061342543975356385, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 5.655857626881963e-16 + } + }, + { + "exponents": [ + 1, + 2, + 2 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.003166045610991512, + "original_integral": 0.003166045610991514, + "original_abs_error": 2.168404344971009e-18, + "original_rel_error": 6.848935901122184e-16, + "reduced_integral": 0.0031660456109915153, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 1.0958297441795495e-15 + } + }, + { + "exponents": [ + 1, + 3, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.007028050634648921, + "original_integral": 0.0070280506346489255, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 6.170713495661459e-16, + "reduced_integral": 0.007028050634648923, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.4682853982645837e-16 + } + }, + { + "exponents": [ + 1, + 3, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.003317140367522541, + "original_integral": 0.003317140367522542, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 2.6147875636514183e-16, + "reduced_integral": 0.003317140367522542, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 2.6147875636514183e-16 + } + }, + { + "exponents": [ + 1, + 4, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.004001792513563149, + "original_integral": 0.004001792513563152, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 6.502299170049535e-16, + "reduced_integral": 0.004001792513563153, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 8.669732226732713e-16 + } + }, + { + "exponents": [ + 2, + 0, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.02801645567318039, + "original_integral": 0.028016455673180413, + "original_abs_error": 2.42861286636753e-17, + "original_rel_error": 8.668522866339563e-16, + "reduced_integral": 0.028016455673180406, + "reduced_abs_error": 1.734723475976807e-17, + "reduced_rel_error": 6.191802047385402e-16 + } + }, + { + "exponents": [ + 2, + 0, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.013223370305591857, + "original_integral": 0.01322337030559187, + "original_abs_error": 1.214306433183765e-17, + "original_rel_error": 9.183032805715672e-16, + "reduced_integral": 0.013223370305591873, + "reduced_abs_error": 1.5612511283791264e-17, + "reduced_rel_error": 1.1806756464491579e-15 + } + }, + { + "exponents": [ + 2, + 0, + 2 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.006824919673261954, + "original_integral": 0.006824919673261958, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 6.35437323450468e-16, + "reduced_integral": 0.006824919673261962, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 1.270874646900936e-15 + } + }, + { + "exponents": [ + 2, + 0, + 3 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.0037722391932440667, + "original_integral": 0.00377223919324407, + "original_abs_error": 3.469446951953614e-18, + "original_rel_error": 9.197314311794593e-16, + "reduced_integral": 0.0037722391932440724, + "reduced_abs_error": 5.637851296924623e-18, + "reduced_rel_error": 1.4945635756666213e-15 + } + }, + { + "exponents": [ + 2, + 1, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.012831563252858731, + "original_integral": 0.012831563252858724, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 5.407676186579464e-16, + "reduced_integral": 0.012831563252858736, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 4.055757139934598e-16 + } + }, + { + "exponents": [ + 2, + 1, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.00605631613332888, + "original_integral": 0.006056316133328885, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 8.592963632283057e-16, + "reduced_integral": 0.006056316133328884, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 7.160803026902547e-16 + } + }, + { + "exponents": [ + 2, + 1, + 2 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.0031258196791457077, + "original_integral": 0.003125819679145711, + "original_abs_error": 3.469446951953614e-18, + "original_rel_error": 1.1099318924570275e-15, + "reduced_integral": 0.003125819679145712, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 1.3874148655712845e-15 + } + }, + { + "exponents": [ + 2, + 2, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.006460544291672678, + "original_integral": 0.006460544291672678, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.006460544291672682, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 6.712760557236563e-16 + } + }, + { + "exponents": [ + 2, + 2, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.0030492854107254594, + "original_integral": 0.003049285410725461, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 5.688950827217243e-16, + "reduced_integral": 0.0030492854107254616, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 7.111188534021553e-16 + } + }, + { + "exponents": [ + 2, + 3, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.0034935838778848544, + "original_integral": 0.0034935838778848544, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0034935838778848557, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 3.724091513069109e-16 + } + }, + { + "exponents": [ + 3, + 0, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.01498059689723164, + "original_integral": 0.014980596897231637, + "original_abs_error": 3.469446951953614e-18, + "original_rel_error": 2.3159604225081016e-16, + "reduced_integral": 0.014980596897231647, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 4.631920845016203e-16 + } + }, + { + "exponents": [ + 3, + 0, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.007070629578620323, + "original_integral": 0.007070629578620329, + "original_abs_error": 6.071532165918825e-18, + "original_rel_error": 8.586975315858012e-16, + "reduced_integral": 0.00707062957862033, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 9.813686075266298e-16 + } + }, + { + "exponents": [ + 3, + 0, + 2 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.0036493327947616466, + "original_integral": 0.003649332794761648, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 3.5651519884680235e-16, + "reduced_integral": 0.0036493327947616505, + "reduced_abs_error": 3.903127820947816e-18, + "reduced_rel_error": 1.069545596540407e-15 + } + }, + { + "exponents": [ + 3, + 1, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.006861127577833471, + "original_integral": 0.006861127577833471, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0068611275778334745, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 5.056671680559476e-16 + } + }, + { + "exponents": [ + 3, + 1, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.003238355048688473, + "original_integral": 0.0032383550486884743, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 4.0176033431217643e-16, + "reduced_integral": 0.003238355048688477, + "reduced_abs_error": 3.903127820947816e-18, + "reduced_rel_error": 1.2052810029365293e-15 + } + }, + { + "exponents": [ + 3, + 2, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.003454498702450346, + "original_integral": 0.0034544987024503473, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 3.7662269378180673e-16, + "reduced_integral": 0.0034544987024503473, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 3.7662269378180673e-16 + } + }, + { + "exponents": [ + 4, + 0, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.008440467836218837, + "original_integral": 0.008440467836218846, + "original_abs_error": 8.673617379884035e-18, + "original_rel_error": 1.027622822358819e-15, + "reduced_integral": 0.008440467836218853, + "reduced_abs_error": 1.5612511283791264e-17, + "reduced_rel_error": 1.8497210802458743e-15 + } + }, + { + "exponents": [ + 4, + 0, + 1 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.003983781283854647, + "original_integral": 0.00398378128385465, + "original_abs_error": 3.469446951953614e-18, + "original_rel_error": 8.708929292916979e-16, + "reduced_integral": 0.003983781283854655, + "reduced_abs_error": 7.806255641895632e-18, + "reduced_rel_error": 1.9595090909063203e-15 + } + }, + { + "exponents": [ + 4, + 1, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.003865742269027963, + "original_integral": 0.0038657422690279654, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 6.73113992832094e-16, + "reduced_integral": 0.003865742269027968, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 1.346227985664188e-15 + } + }, + { + "exponents": [ + 5, + 0, + 0 + ], + "result": { + "original_strength": 5, + "reduced_strength": 5, + "original_points": 84, + "reduced_points": 56, + "exact_integral": 0.0049366547789406945, + "original_integral": 0.004936654778940698, + "original_abs_error": 3.469446951953614e-18, + "original_rel_error": 7.02793107339397e-16, + "reduced_integral": 0.004936654778940703, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 1.7569827683484922e-15 + } + } + ] + }, + { + "strength": 6, + "results": [ + { + "exponents": [ + 0, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.12499999999999997, + "original_integral": 0.125, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 2.2204460492503136e-16, + "reduced_integral": 0.12500000000000006, + "reduced_abs_error": 8.326672684688674e-17, + "reduced_rel_error": 6.661338147750941e-16 + } + }, + { + "exponents": [ + 0, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.05899822973615082, + "original_integral": 0.058998229736150876, + "original_abs_error": 5.551115123125783e-17, + "original_rel_error": 9.408952010850538e-16, + "reduced_integral": 0.05899822973615084, + "reduced_abs_error": 2.0816681711721685e-17, + "reduced_rel_error": 3.5283570040689516e-16 + } + }, + { + "exponents": [ + 0, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.030450495562663715, + "original_integral": 0.030450495562663757, + "original_abs_error": 4.163336342344337e-17, + "original_rel_error": 1.367247483305044e-15, + "reduced_integral": 0.030450495562663733, + "reduced_abs_error": 1.734723475976807e-17, + "reduced_rel_error": 5.696864513771016e-16 + } + }, + { + "exponents": [ + 0, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.016830462234625018, + "original_integral": 0.016830462234625046, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 1.649127351863685e-15, + "reduced_integral": 0.016830462234625032, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 8.245636759318425e-16 + } + }, + { + "exponents": [ + 0, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.00978179162587195, + "original_integral": 0.00978179162587195, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.009781791625871962, + "reduced_abs_error": 1.214306433183765e-17, + "reduced_rel_error": 1.2413947052112978e-15 + } + }, + { + "exponents": [ + 0, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0058964926575299035, + "original_integral": 0.005896492657529905, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.941958171967773e-16, + "reduced_integral": 0.005896492657529905, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.941958171967773e-16 + } + }, + { + "exponents": [ + 0, + 0, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0036511518202430055, + "original_integral": 0.0036511518202430085, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 8.314543553429571e-16, + "reduced_integral": 0.0036511518202430085, + "reduced_abs_error": 3.0357660829594124e-18, + "reduced_rel_error": 8.314543553429571e-16 + } + }, + { + "exponents": [ + 0, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.057250118477433484, + "original_integral": 0.05725011847743346, + "original_abs_error": 2.0816681711721685e-17, + "original_rel_error": 3.6360940842292024e-16, + "reduced_integral": 0.057250118477433505, + "reduced_abs_error": 2.0816681711721685e-17, + "reduced_rel_error": 3.6360940842292024e-16 + } + }, + { + "exponents": [ + 0, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.027021245138827792, + "original_integral": 0.02702124513882782, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 1.027176041408467e-15, + "reduced_integral": 0.02702124513882782, + "reduced_abs_error": 2.7755575615628914e-17, + "reduced_rel_error": 1.027176041408467e-15 + } + }, + { + "exponents": [ + 0, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.013946355829272483, + "original_integral": 0.01394635582927249, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 4.975417226443446e-16, + "reduced_integral": 0.013946355829272495, + "reduced_abs_error": 1.214306433183765e-17, + "reduced_rel_error": 8.706980146276031e-16 + } + }, + { + "exponents": [ + 0, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0077083676556980165, + "original_integral": 0.0077083676556980165, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.007708367655698024, + "reduced_abs_error": 7.806255641895632e-18, + "reduced_rel_error": 1.0126989254495739e-15 + } + }, + { + "exponents": [ + 0, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.004480069836021888, + "original_integral": 0.004480069836021894, + "original_abs_error": 6.071532165918825e-18, + "original_rel_error": 1.3552315897178265e-15, + "reduced_integral": 0.004480069836021894, + "reduced_abs_error": 6.071532165918825e-18, + "reduced_rel_error": 1.3552315897178265e-15 + } + }, + { + "exponents": [ + 0, + 1, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.002700599225959228, + "original_integral": 0.002700599225959227, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 3.211738082611368e-16, + "reduced_integral": 0.0027005992259592306, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 9.635214247834104e-16 + } + }, + { + "exponents": [ + 0, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.02882477519210804, + "original_integral": 0.028824775192108076, + "original_abs_error": 3.469446951953614e-17, + "original_rel_error": 1.2036336550175477e-15, + "reduced_integral": 0.028824775192108052, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 3.6109009650526434e-16 + } + }, + { + "exponents": [ + 0, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.013604885671015126, + "original_integral": 0.013604885671015147, + "original_abs_error": 2.0816681711721685e-17, + "original_rel_error": 1.5300886913052942e-15, + "reduced_integral": 0.013604885671015136, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 7.650443456526471e-16 + } + }, + { + "exponents": [ + 0, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.007021829512656519, + "original_integral": 0.007021829512656529, + "original_abs_error": 9.540979117872439e-18, + "original_rel_error": 1.3587597221885365e-15, + "reduced_integral": 0.007021829512656526, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 9.881888888643901e-16 + } + }, + { + "exponents": [ + 0, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.003881074322338644, + "original_integral": 0.003881074322338643, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 2.234849595628851e-16, + "reduced_integral": 0.0038810743223386475, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 8.939398382515404e-16 + } + }, + { + "exponents": [ + 0, + 2, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0022556635567344318, + "original_integral": 0.002255663556734433, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 5.767893013558059e-16, + "reduced_integral": 0.0022556635567344344, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 1.1535786027116119e-15 + } + }, + { + "exponents": [ + 0, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.015587195961894966, + "original_integral": 0.015587195961894949, + "original_abs_error": 1.734723475976807e-17, + "original_rel_error": 1.1129156778535254e-15, + "reduced_integral": 0.015587195961894973, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 4.451662711414102e-16 + } + }, + { + "exponents": [ + 0, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.007356935746418251, + "original_integral": 0.0073569357464182555, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 5.894857369188534e-16, + "reduced_integral": 0.007356935746418258, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 9.431771790701655e-16 + } + }, + { + "exponents": [ + 0, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0037971027317764182, + "original_integral": 0.003797102731776424, + "original_abs_error": 5.637851296924623e-18, + "original_rel_error": 1.4847771301375978e-15, + "reduced_integral": 0.003797102731776424, + "reduced_abs_error": 5.637851296924623e-18, + "reduced_rel_error": 1.4847771301375978e-15 + } + }, + { + "exponents": [ + 0, + 3, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.002098717703842982, + "original_integral": 0.0020987177038429843, + "original_abs_error": 2.168404344971009e-18, + "original_rel_error": 1.0332043899951017e-15, + "reduced_integral": 0.0020987177038429843, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 1.0332043899951017e-15 + } + }, + { + "exponents": [ + 0, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.008875394807235764, + "original_integral": 0.008875394807235766, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.9545310531567051e-16, + "reduced_integral": 0.008875394807235776, + "reduced_abs_error": 1.214306433183765e-17, + "reduced_rel_error": 1.3681717372096937e-15 + } + }, + { + "exponents": [ + 0, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.004189060654690686, + "original_integral": 0.004189060654690683, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 6.211619808014799e-16, + "reduced_integral": 0.0041890606546906885, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 6.211619808014799e-16 + } + }, + { + "exponents": [ + 0, + 4, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.00216208136155697, + "original_integral": 0.0021620813615569707, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 4.01169795647188e-16, + "reduced_integral": 0.0021620813615569716, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 8.02339591294376e-16 + } + }, + { + "exponents": [ + 0, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.005244601150649224, + "original_integral": 0.005244601150649229, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 9.922909823726447e-16, + "reduced_integral": 0.00524460115064923, + "reduced_abs_error": 6.071532165918825e-18, + "reduced_rel_error": 1.1576728127680855e-15 + } + }, + { + "exponents": [ + 0, + 5, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.002475377468483872, + "original_integral": 0.0024753774684838697, + "original_abs_error": 2.168404344971009e-18, + "original_rel_error": 8.759893683201055e-16, + "reduced_integral": 0.0024753774684838754, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 1.401582989312169e-15 + } + }, + { + "exponents": [ + 0, + 6, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0031841740489256596, + "original_integral": 0.0031841740489256583, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 4.0859657386554515e-16, + "reduced_integral": 0.003184174048925665, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 1.6343862954621806e-15 + } + }, + { + "exponents": [ + 1, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.056360767612003086, + "original_integral": 0.05636076761200307, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 2.4623134843285776e-16, + "reduced_integral": 0.05636076761200311, + "reduced_abs_error": 2.0816681711721685e-17, + "reduced_rel_error": 3.6934702264928667e-16 + } + }, + { + "exponents": [ + 1, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.02660148412543013, + "original_integral": 0.026601484125430137, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 2.6084611938150766e-16, + "reduced_integral": 0.026601484125430144, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 5.216922387630153e-16 + } + }, + { + "exponents": [ + 1, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.013729706432620965, + "original_integral": 0.013729706432620954, + "original_abs_error": 1.0408340855860843e-17, + "original_rel_error": 7.580891045952188e-16, + "reduced_integral": 0.013729706432620972, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 5.053927363968126e-16 + } + }, + { + "exponents": [ + 1, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.007588622166466357, + "original_integral": 0.007588622166466358, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.1429765759339295e-16, + "reduced_integral": 0.0075886221664663595, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 3.428929727801789e-16 + } + }, + { + "exponents": [ + 1, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.004410474277238454, + "original_integral": 0.004410474277238458, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 9.832975814695012e-16, + "reduced_integral": 0.004410474277238457, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 7.86638065175601e-16 + } + }, + { + "exponents": [ + 1, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0026586468191754017, + "original_integral": 0.0026586468191754043, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 9.78725415951362e-16, + "reduced_integral": 0.002658646819175402, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 1.6312090265856034e-16 + } + }, + { + "exponents": [ + 1, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.02581328498613018, + "original_integral": 0.025813284986130173, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 2.6881095945888283e-16, + "reduced_integral": 0.02581328498613019, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 4.032164391883242e-16 + } + }, + { + "exponents": [ + 1, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.01218350494285153, + "original_integral": 0.012183504942851536, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 4.271488748386713e-16, + "reduced_integral": 0.012183504942851532, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 1.423829582795571e-16 + } + }, + { + "exponents": [ + 1, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.006288218559423448, + "original_integral": 0.006288218559423449, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.758688266929227e-16, + "reduced_integral": 0.006288218559423449, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.758688266929227e-16 + } + }, + { + "exponents": [ + 1, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0034755961448854167, + "original_integral": 0.003475596144885417, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.2477884394951743e-16, + "reduced_integral": 0.0034755961448854175, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 2.4955768789903486e-16 + } + }, + { + "exponents": [ + 1, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.002020001399308595, + "original_integral": 0.002020001399308598, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 1.502853455447354e-15, + "reduced_integral": 0.002020001399308596, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 6.440800523345803e-16 + } + }, + { + "exponents": [ + 1, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.012996691648565066, + "original_integral": 0.012996691648565059, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 5.338969402011884e-16, + "reduced_integral": 0.012996691648565066, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 1, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.006134254397535635, + "original_integral": 0.006134254397535638, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 4.241893220161472e-16, + "reduced_integral": 0.006134254397535639, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 7.069822033602453e-16 + } + }, + { + "exponents": [ + 1, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.003166045610991512, + "original_integral": 0.0031660456109915158, + "original_abs_error": 3.903127820947816e-18, + "original_rel_error": 1.2328084622019933e-15, + "reduced_integral": 0.003166045610991515, + "reduced_abs_error": 3.0357660829594124e-18, + "reduced_rel_error": 9.58851026157106e-16 + } + }, + { + "exponents": [ + 1, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.001749922623729925, + "original_integral": 0.0017499226237299257, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 3.717428957542874e-16, + "reduced_integral": 0.0017499226237299263, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 7.434857915085748e-16 + } + }, + { + "exponents": [ + 1, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.007028050634648921, + "original_integral": 0.007028050634648921, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.007028050634648923, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.4682853982645837e-16 + } + }, + { + "exponents": [ + 1, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.003317140367522541, + "original_integral": 0.0033171403675225425, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 3.9221813454771275e-16, + "reduced_integral": 0.0033171403675225425, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 3.9221813454771275e-16 + } + }, + { + "exponents": [ + 1, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0017120609973164226, + "original_integral": 0.0017120609973164224, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.2665461968760946e-16, + "reduced_integral": 0.0017120609973164242, + "reduced_abs_error": 1.5178830414797062e-18, + "reduced_rel_error": 8.865823378132663e-16 + } + }, + { + "exponents": [ + 1, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.004001792513563149, + "original_integral": 0.004001792513563149, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.004001792513563154, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 1.300459834009907e-15 + } + }, + { + "exponents": [ + 1, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0018887893925728578, + "original_integral": 0.0018887893925728573, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.29607848656675e-16, + "reduced_integral": 0.001888789392572859, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 6.88823545970025e-16 + } + }, + { + "exponents": [ + 1, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0023647179733550794, + "original_integral": 0.0023647179733550763, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 1.2837751127895581e-15, + "reduced_integral": 0.0023647179733550816, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 9.16982223421113e-16 + } + }, + { + "exponents": [ + 2, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.02801645567318039, + "original_integral": 0.028016455673180386, + "original_abs_error": 3.469446951953614e-18, + "original_rel_error": 1.2383604094770806e-16, + "reduced_integral": 0.028016455673180403, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 4.953441637908322e-16 + } + }, + { + "exponents": [ + 2, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.013223370305591857, + "original_integral": 0.013223370305591873, + "original_abs_error": 1.5612511283791264e-17, + "original_rel_error": 1.1806756464491579e-15, + "reduced_integral": 0.013223370305591868, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 7.871170976327719e-16 + } + }, + { + "exponents": [ + 2, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.006824919673261954, + "original_integral": 0.006824919673261962, + "original_abs_error": 8.673617379884035e-18, + "original_rel_error": 1.270874646900936e-15, + "reduced_integral": 0.006824919673261954, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 2, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0037722391932440667, + "original_integral": 0.003772239193244071, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 1.149664288974324e-15, + "reduced_integral": 0.003772239193244068, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 3.4489928669229722e-16 + } + }, + { + "exponents": [ + 2, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0021924090519242294, + "original_integral": 0.0021924090519242294, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0021924090519242294, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 2, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.012831563252858731, + "original_integral": 0.01283156325285873, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.351919046644866e-16, + "reduced_integral": 0.01283156325285874, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 6.75959523322433e-16 + } + }, + { + "exponents": [ + 2, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.00605631613332888, + "original_integral": 0.006056316133328876, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 7.160803026902547e-16, + "reduced_integral": 0.006056316133328881, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 1.4321606053805094e-16 + } + }, + { + "exponents": [ + 2, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0031258196791457077, + "original_integral": 0.003125819679145709, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 4.1622445967138537e-16, + "reduced_integral": 0.0031258196791457095, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 5.549659462285138e-16 + } + }, + { + "exponents": [ + 2, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0017276891259075272, + "original_integral": 0.0017276891259075291, + "original_abs_error": 1.951563910473908e-18, + "original_rel_error": 1.1295804790394702e-15, + "reduced_integral": 0.0017276891259075287, + "reduced_abs_error": 1.5178830414797062e-18, + "reduced_rel_error": 8.785625948084767e-16 + } + }, + { + "exponents": [ + 2, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.006460544291672678, + "original_integral": 0.006460544291672669, + "original_abs_error": 8.673617379884035e-18, + "original_rel_error": 1.3425521114473126e-15, + "reduced_integral": 0.00646054429167268, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 4.0276563343419377e-16 + } + }, + { + "exponents": [ + 2, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0030492854107254594, + "original_integral": 0.003049285410725459, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.4222377068043107e-16, + "reduced_integral": 0.0030492854107254594, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 2, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0015738142022877699, + "original_integral": 0.0015738142022877703, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.7556039865683194e-16, + "reduced_integral": 0.0015738142022877712, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 8.266811959704958e-16 + } + }, + { + "exponents": [ + 2, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0034935838778848544, + "original_integral": 0.003493583877884855, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.241363837689703e-16, + "reduced_integral": 0.0034935838778848557, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 3.724091513069109e-16 + } + }, + { + "exponents": [ + 2, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.001648922113839706, + "original_integral": 0.0016489221138397056, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.630087045071678e-16, + "reduced_integral": 0.0016489221138397067, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 3.945130567607517e-16 + } + }, + { + "exponents": [ + 2, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.001989256841591169, + "original_integral": 0.0019892568415911713, + "original_abs_error": 2.168404344971009e-18, + "original_rel_error": 1.0900575026986173e-15, + "reduced_integral": 0.0019892568415911713, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 1.0900575026986173e-15 + } + }, + { + "exponents": [ + 3, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.01498059689723164, + "original_integral": 0.014980596897231625, + "original_abs_error": 1.5612511283791264e-17, + "original_rel_error": 1.0421821901286456e-15, + "reduced_integral": 0.014980596897231644, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 2.3159604225081016e-16 + } + }, + { + "exponents": [ + 3, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.007070629578620323, + "original_integral": 0.007070629578620324, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.2267107594082873e-16, + "reduced_integral": 0.0070706295786203245, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.4534215188165746e-16 + } + }, + { + "exponents": [ + 3, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0036493327947616466, + "original_integral": 0.0036493327947616453, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 3.5651519884680235e-16, + "reduced_integral": 0.003649332794761648, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 3.5651519884680235e-16 + } + }, + { + "exponents": [ + 3, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.002017042962647982, + "original_integral": 0.0020170429626479823, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.150082457464683e-16, + "reduced_integral": 0.002017042962647984, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 1.0750412287323415e-15 + } + }, + { + "exponents": [ + 3, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.006861127577833471, + "original_integral": 0.006861127577833473, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.528335840279738e-16, + "reduced_integral": 0.006861127577833473, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.528335840279738e-16 + } + }, + { + "exponents": [ + 3, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.003238355048688473, + "original_integral": 0.0032383550486884717, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 4.0176033431217643e-16, + "reduced_integral": 0.003238355048688474, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 2.678402228747843e-16 + } + }, + { + "exponents": [ + 3, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0016713978789095016, + "original_integral": 0.0016713978789095034, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.0378878050920001e-15, + "reduced_integral": 0.0016713978789095027, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 6.486798781825e-16 + } + }, + { + "exponents": [ + 3, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.003454498702450346, + "original_integral": 0.00345449870245035, + "original_abs_error": 3.903127820947816e-18, + "original_rel_error": 1.1298680813454201e-15, + "reduced_integral": 0.003454498702450348, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 6.277044896363445e-16 + } + }, + { + "exponents": [ + 3, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0016304744645632033, + "original_integral": 0.0016304744645632041, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 5.319689187654747e-16, + "reduced_integral": 0.001630474464563204, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 3.9897668907410605e-16 + } + }, + { + "exponents": [ + 3, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0018680439957064418, + "original_integral": 0.001868043995706441, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 4.643154764994664e-16, + "reduced_integral": 0.0018680439957064429, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 5.803943456243329e-16 + } + }, + { + "exponents": [ + 4, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.008440467836218837, + "original_integral": 0.008440467836218851, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 1.6441965157741104e-15, + "reduced_integral": 0.008440467836218846, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 1.027622822358819e-15 + } + }, + { + "exponents": [ + 4, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.003983781283854647, + "original_integral": 0.003983781283854649, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 4.3544646464584893e-16, + "reduced_integral": 0.003983781283854649, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 4.3544646464584893e-16 + } + }, + { + "exponents": [ + 4, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0020561314271487006, + "original_integral": 0.0020561314271487037, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 1.4764455437409468e-15, + "reduced_integral": 0.002056131427148701, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 2.1092079196299242e-16 + } + }, + { + "exponents": [ + 4, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.003865742269027963, + "original_integral": 0.0038657422690279637, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 2.243713309440313e-16, + "reduced_integral": 0.003865742269027966, + "reduced_abs_error": 3.0357660829594124e-18, + "reduced_rel_error": 7.852996583041096e-16 + } + }, + { + "exponents": [ + 4, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.001824575603910886, + "original_integral": 0.0018245756039108856, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.3768862636584016e-16, + "reduced_integral": 0.0018245756039108865, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 2.3768862636584016e-16 + } + }, + { + "exponents": [ + 4, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.001946356703161814, + "original_integral": 0.0019463567031618122, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 8.91266987782243e-16, + "reduced_integral": 0.001946356703161815, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 5.570418673639018e-16 + } + }, + { + "exponents": [ + 5, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0049366547789406945, + "original_integral": 0.004936654778940692, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 5.270948305045477e-16, + "reduced_integral": 0.0049366547789407005, + "reduced_abs_error": 6.071532165918825e-18, + "reduced_rel_error": 1.2298879378439447e-15 + } + }, + { + "exponents": [ + 5, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.00233003114220808, + "original_integral": 0.002330031142208081, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 3.7225328120139994e-16, + "reduced_integral": 0.002330031142208081, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 3.7225328120139994e-16 + } + }, + { + "exponents": [ + 5, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0022609925678123447, + "original_integral": 0.0022609925678123474, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 1.1508596936622816e-15, + "reduced_integral": 0.002260992567812346, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 5.754298468311408e-16 + } + }, + { + "exponents": [ + 6, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 6, + "original_points": 486, + "reduced_points": 84, + "exact_integral": 0.0029669160778858826, + "original_integral": 0.00296691607788588, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 8.770336422253516e-16, + "reduced_integral": 0.002966916077885884, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 4.385168211126758e-16 + } + } + ] + }, + { + "strength": 7, + "results": [ + { + "exponents": [ + 0, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.12499999999999997, + "original_integral": 0.125, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 2.2204460492503136e-16, + "reduced_integral": 0.125, + "reduced_abs_error": 2.7755575615628914e-17, + "reduced_rel_error": 2.2204460492503136e-16 + } + }, + { + "exponents": [ + 0, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.05899822973615082, + "original_integral": 0.058998229736150876, + "original_abs_error": 5.551115123125783e-17, + "original_rel_error": 9.408952010850538e-16, + "reduced_integral": 0.05899822973615078, + "reduced_abs_error": 4.163336342344337e-17, + "reduced_rel_error": 7.056714008137903e-16 + } + }, + { + "exponents": [ + 0, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.030450495562663715, + "original_integral": 0.030450495562663757, + "original_abs_error": 4.163336342344337e-17, + "original_rel_error": 1.367247483305044e-15, + "reduced_integral": 0.030450495562663715, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 0, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.016830462234625018, + "original_integral": 0.016830462234625046, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 1.649127351863685e-15, + "reduced_integral": 0.016830462234625008, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 6.184227569488818e-16 + } + }, + { + "exponents": [ + 0, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.00978179162587195, + "original_integral": 0.00978179162587195, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.00978179162587195, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 0, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0058964926575299035, + "original_integral": 0.005896492657529905, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.941958171967773e-16, + "reduced_integral": 0.0058964926575299, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 5.883916343935546e-16 + } + }, + { + "exponents": [ + 0, + 0, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0036511518202430055, + "original_integral": 0.0036511518202430085, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 8.314543553429571e-16, + "reduced_integral": 0.003651151820243004, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 3.563375808612673e-16 + } + }, + { + "exponents": [ + 0, + 0, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.002306984964566927, + "original_integral": 0.0023069849645669313, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 1.879859971586826e-15, + "reduced_integral": 0.0023069849645669265, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 1.879859971586826e-16 + } + }, + { + "exponents": [ + 0, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.057250118477433484, + "original_integral": 0.05725011847743346, + "original_abs_error": 2.0816681711721685e-17, + "original_rel_error": 3.6360940842292024e-16, + "reduced_integral": 0.05725011847743346, + "reduced_abs_error": 2.0816681711721685e-17, + "reduced_rel_error": 3.6360940842292024e-16 + } + }, + { + "exponents": [ + 0, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.027021245138827792, + "original_integral": 0.02702124513882782, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 1.027176041408467e-15, + "reduced_integral": 0.02702124513882776, + "reduced_abs_error": 3.122502256758253e-17, + "reduced_rel_error": 1.1555730465845253e-15 + } + }, + { + "exponents": [ + 0, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.013946355829272483, + "original_integral": 0.01394635582927249, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 4.975417226443446e-16, + "reduced_integral": 0.013946355829272472, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 7.463125839665169e-16 + } + }, + { + "exponents": [ + 0, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0077083676556980165, + "original_integral": 0.0077083676556980165, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.007708367655698013, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 4.500884113109217e-16 + } + }, + { + "exponents": [ + 0, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.004480069836021888, + "original_integral": 0.004480069836021894, + "original_abs_error": 6.071532165918825e-18, + "original_rel_error": 1.3552315897178265e-15, + "reduced_integral": 0.004480069836021886, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 3.872090256336647e-16 + } + }, + { + "exponents": [ + 0, + 1, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.002700599225959228, + "original_integral": 0.002700599225959227, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 3.211738082611368e-16, + "reduced_integral": 0.0027005992259592267, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 4.817607123917052e-16 + } + }, + { + "exponents": [ + 0, + 1, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0016722309943040716, + "original_integral": 0.001672230994304074, + "original_abs_error": 2.3852447794681098e-18, + "original_rel_error": 1.4263847444478037e-15, + "reduced_integral": 0.0016722309943040705, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 6.48356702021729e-16 + } + }, + { + "exponents": [ + 0, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.02882477519210804, + "original_integral": 0.028824775192108076, + "original_abs_error": 3.469446951953614e-17, + "original_rel_error": 1.2036336550175477e-15, + "reduced_integral": 0.02882477519210802, + "reduced_abs_error": 2.0816681711721685e-17, + "reduced_rel_error": 7.221801930105287e-16 + } + }, + { + "exponents": [ + 0, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.013604885671015126, + "original_integral": 0.013604885671015147, + "original_abs_error": 2.0816681711721685e-17, + "original_rel_error": 1.5300886913052942e-15, + "reduced_integral": 0.013604885671015114, + "reduced_abs_error": 1.214306433183765e-17, + "reduced_rel_error": 8.92551736594755e-16 + } + }, + { + "exponents": [ + 0, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.007021829512656519, + "original_integral": 0.007021829512656529, + "original_abs_error": 9.540979117872439e-18, + "original_rel_error": 1.3587597221885365e-15, + "reduced_integral": 0.007021829512656513, + "reduced_abs_error": 6.071532165918825e-18, + "reduced_rel_error": 8.646652777563415e-16 + } + }, + { + "exponents": [ + 0, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.003881074322338644, + "original_integral": 0.003881074322338643, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 2.234849595628851e-16, + "reduced_integral": 0.00388107432233864, + "reduced_abs_error": 3.903127820947816e-18, + "reduced_rel_error": 1.005682318032983e-15 + } + }, + { + "exponents": [ + 0, + 2, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0022556635567344318, + "original_integral": 0.002255663556734433, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 5.767893013558059e-16, + "reduced_integral": 0.0022556635567344296, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 9.613155022596767e-16 + } + }, + { + "exponents": [ + 0, + 2, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0013597206022017202, + "original_integral": 0.001359720602201722, + "original_abs_error": 1.951563910473908e-18, + "original_rel_error": 1.4352683244733137e-15, + "reduced_integral": 0.0013597206022017189, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 9.568455496488758e-16 + } + }, + { + "exponents": [ + 0, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.015587195961894966, + "original_integral": 0.015587195961894949, + "original_abs_error": 1.734723475976807e-17, + "original_rel_error": 1.1129156778535254e-15, + "reduced_integral": 0.015587195961894956, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 6.677494067121153e-16 + } + }, + { + "exponents": [ + 0, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.007356935746418251, + "original_integral": 0.0073569357464182555, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 5.894857369188534e-16, + "reduced_integral": 0.007356935746418243, + "reduced_abs_error": 7.806255641895632e-18, + "reduced_rel_error": 1.0610743264539362e-15 + } + }, + { + "exponents": [ + 0, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0037971027317764182, + "original_integral": 0.003797102731776424, + "original_abs_error": 5.637851296924623e-18, + "original_rel_error": 1.4847771301375978e-15, + "reduced_integral": 0.003797102731776414, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 1.1421362539519982e-15 + } + }, + { + "exponents": [ + 0, + 3, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.002098717703842982, + "original_integral": 0.0020987177038429843, + "original_abs_error": 2.168404344971009e-18, + "original_rel_error": 1.0332043899951017e-15, + "reduced_integral": 0.002098717703842979, + "reduced_abs_error": 3.0357660829594124e-18, + "reduced_rel_error": 1.4464861459931424e-15 + } + }, + { + "exponents": [ + 0, + 3, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.001219765623447114, + "original_integral": 0.001219765623447115, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 7.110888529037235e-16, + "reduced_integral": 0.001219765623447112, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 1.7777221322593087e-15 + } + }, + { + "exponents": [ + 0, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.008875394807235764, + "original_integral": 0.008875394807235766, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.9545310531567051e-16, + "reduced_integral": 0.008875394807235757, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 7.818124212626821e-16 + } + }, + { + "exponents": [ + 0, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.004189060654690686, + "original_integral": 0.004189060654690683, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 6.211619808014799e-16, + "reduced_integral": 0.004189060654690679, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 1.6564319488039465e-15 + } + }, + { + "exponents": [ + 0, + 4, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.00216208136155697, + "original_integral": 0.0021620813615569707, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 4.01169795647188e-16, + "reduced_integral": 0.002162081361556965, + "reduced_abs_error": 4.7704895589362195e-18, + "reduced_rel_error": 2.206433876059534e-15 + } + }, + { + "exponents": [ + 0, + 4, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0011950159769645481, + "original_integral": 0.0011950159769645499, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.4516320362370101e-15, + "reduced_integral": 0.0011950159769645453, + "reduced_abs_error": 2.8189256484623115e-18, + "reduced_rel_error": 2.3589020588851417e-15 + } + }, + { + "exponents": [ + 0, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.005244601150649224, + "original_integral": 0.005244601150649229, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 9.922909823726447e-16, + "reduced_integral": 0.005244601150649217, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 1.3230546431635263e-15 + } + }, + { + "exponents": [ + 0, + 5, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.002475377468483872, + "original_integral": 0.0024753774684838697, + "original_abs_error": 2.168404344971009e-18, + "original_rel_error": 8.759893683201055e-16, + "reduced_integral": 0.002475377468483865, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 2.803165978624338e-15 + } + }, + { + "exponents": [ + 0, + 5, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.001277605632526282, + "original_integral": 0.001277605632526281, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 8.486203761810678e-16, + "reduced_integral": 0.001277605632526278, + "reduced_abs_error": 4.119968255444917e-18, + "reduced_rel_error": 3.2247574294880573e-15 + } + }, + { + "exponents": [ + 0, + 6, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0031841740489256596, + "original_integral": 0.0031841740489256583, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 4.0859657386554515e-16, + "reduced_integral": 0.003184174048925654, + "reduced_abs_error": 5.637851296924623e-18, + "reduced_rel_error": 1.7705851534173623e-15 + } + }, + { + "exponents": [ + 0, + 6, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0015028850564672448, + "original_integral": 0.0015028850564672465, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.1542622428188441e-15, + "reduced_integral": 0.0015028850564672393, + "reduced_abs_error": 5.421010862427522e-18, + "reduced_rel_error": 3.607069508808888e-15 + } + }, + { + "exponents": [ + 0, + 7, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.001972861658829849, + "original_integral": 0.0019728616588298494, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.198232537254681e-16, + "reduced_integral": 0.0019728616588298455, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 1.7585860298037448e-15 + } + }, + { + "exponents": [ + 1, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.056360767612003086, + "original_integral": 0.05636076761200307, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 2.4623134843285776e-16, + "reduced_integral": 0.05636076761200308, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 1.2311567421642888e-16 + } + }, + { + "exponents": [ + 1, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.02660148412543013, + "original_integral": 0.026601484125430137, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 2.6084611938150766e-16, + "reduced_integral": 0.02660148412543012, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 3.9126917907226146e-16 + } + }, + { + "exponents": [ + 1, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.013729706432620965, + "original_integral": 0.013729706432620954, + "original_abs_error": 1.0408340855860843e-17, + "original_rel_error": 7.580891045952188e-16, + "reduced_integral": 0.01372970643262097, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 3.790445522976094e-16 + } + }, + { + "exponents": [ + 1, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.007588622166466357, + "original_integral": 0.007588622166466358, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.1429765759339295e-16, + "reduced_integral": 0.00758862216646636, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 4.571906303735718e-16 + } + }, + { + "exponents": [ + 1, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.004410474277238454, + "original_integral": 0.004410474277238458, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 9.832975814695012e-16, + "reduced_integral": 0.0044104742772384554, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 3.933190325878005e-16 + } + }, + { + "exponents": [ + 1, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0026586468191754017, + "original_integral": 0.0026586468191754043, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 9.78725415951362e-16, + "reduced_integral": 0.0026586468191754026, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 3.262418053171207e-16 + } + }, + { + "exponents": [ + 1, + 0, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0016462537540548646, + "original_integral": 0.0016462537540548675, + "original_abs_error": 2.8189256484623115e-18, + "original_rel_error": 1.7123275445957557e-15, + "reduced_integral": 0.0016462537540548655, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 5.26870013721771e-16 + } + }, + { + "exponents": [ + 1, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.02581328498613018, + "original_integral": 0.025813284986130173, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 2.6881095945888283e-16, + "reduced_integral": 0.025813284986130162, + "reduced_abs_error": 1.734723475976807e-17, + "reduced_rel_error": 6.720273986472071e-16 + } + }, + { + "exponents": [ + 1, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.01218350494285153, + "original_integral": 0.012183504942851536, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 4.271488748386713e-16, + "reduced_integral": 0.012183504942851522, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 7.119147913977855e-16 + } + }, + { + "exponents": [ + 1, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.006288218559423448, + "original_integral": 0.006288218559423449, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.758688266929227e-16, + "reduced_integral": 0.006288218559423444, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 5.517376533858454e-16 + } + }, + { + "exponents": [ + 1, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0034755961448854167, + "original_integral": 0.003475596144885417, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.2477884394951743e-16, + "reduced_integral": 0.0034755961448854145, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 6.238942197475872e-16 + } + }, + { + "exponents": [ + 1, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.002020001399308595, + "original_integral": 0.002020001399308598, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 1.502853455447354e-15, + "reduced_integral": 0.002020001399308594, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 4.293867015563869e-16 + } + }, + { + "exponents": [ + 1, + 1, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0012176627630995475, + "original_integral": 0.0012176627630995486, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 8.903960976236797e-16, + "reduced_integral": 0.0012176627630995477, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 1.7807921952473598e-16 + } + }, + { + "exponents": [ + 1, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.012996691648565066, + "original_integral": 0.012996691648565059, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 5.338969402011884e-16, + "reduced_integral": 0.01299669164856505, + "reduced_abs_error": 1.5612511283791264e-17, + "reduced_rel_error": 1.201268115452674e-15 + } + }, + { + "exponents": [ + 1, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.006134254397535635, + "original_integral": 0.006134254397535638, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 4.241893220161472e-16, + "reduced_integral": 0.006134254397535629, + "reduced_abs_error": 6.071532165918825e-18, + "reduced_rel_error": 9.897750847043436e-16 + } + }, + { + "exponents": [ + 1, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.003166045610991512, + "original_integral": 0.0031660456109915158, + "original_abs_error": 3.903127820947816e-18, + "original_rel_error": 1.2328084622019933e-15, + "reduced_integral": 0.0031660456109915084, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 1.0958297441795495e-15 + } + }, + { + "exponents": [ + 1, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.001749922623729925, + "original_integral": 0.0017499226237299257, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 3.717428957542874e-16, + "reduced_integral": 0.0017499226237299235, + "reduced_abs_error": 1.5178830414797062e-18, + "reduced_rel_error": 8.674000900933374e-16 + } + }, + { + "exponents": [ + 1, + 2, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0010170474362557894, + "original_integral": 0.001017047436255791, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 1.4924407528794515e-15, + "reduced_integral": 0.0010170474362557887, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 6.396174655197649e-16 + } + }, + { + "exponents": [ + 1, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.007028050634648921, + "original_integral": 0.007028050634648921, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0070280506346489125, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 1.2341426991322917e-15 + } + }, + { + "exponents": [ + 1, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.003317140367522541, + "original_integral": 0.0033171403675225425, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 3.9221813454771275e-16, + "reduced_integral": 0.0033171403675225372, + "reduced_abs_error": 3.903127820947816e-18, + "reduced_rel_error": 1.1766544036431382e-15 + } + }, + { + "exponents": [ + 1, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0017120609973164226, + "original_integral": 0.0017120609973164224, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.2665461968760946e-16, + "reduced_integral": 0.00171206099731642, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 1.5198554362513137e-15 + } + }, + { + "exponents": [ + 1, + 3, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.000946282726315928, + "original_integral": 0.0009462827263159287, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 8.020240670508353e-16, + "reduced_integral": 0.0009462827263159266, + "reduced_abs_error": 1.4094628242311558e-18, + "reduced_rel_error": 1.4894732673801228e-15 + } + }, + { + "exponents": [ + 1, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.004001792513563149, + "original_integral": 0.004001792513563149, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.004001792513563145, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 1.0837165283415892e-15 + } + }, + { + "exponents": [ + 1, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0018887893925728578, + "original_integral": 0.0018887893925728573, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.29607848656675e-16, + "reduced_integral": 0.0018887893925728545, + "reduced_abs_error": 3.2526065174565133e-18, + "reduced_rel_error": 1.7220588649250622e-15 + } + }, + { + "exponents": [ + 1, + 4, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0009748525214156445, + "original_integral": 0.0009748525214156456, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 1.1121704551895363e-15, + "reduced_integral": 0.0009748525214156426, + "reduced_abs_error": 1.951563910473908e-18, + "reduced_rel_error": 2.001906819341165e-15 + } + }, + { + "exponents": [ + 1, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0023647179733550794, + "original_integral": 0.0023647179733550763, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 1.2837751127895581e-15, + "reduced_integral": 0.0023647179733550763, + "reduced_abs_error": 3.0357660829594124e-18, + "reduced_rel_error": 1.2837751127895581e-15 + } + }, + { + "exponents": [ + 1, + 5, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0011161133940256634, + "original_integral": 0.0011161133940256634, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0011161133940256604, + "reduced_abs_error": 3.0357660829594124e-18, + "reduced_rel_error": 2.719944137584294e-15 + } + }, + { + "exponents": [ + 1, + 6, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0014356999488613598, + "original_integral": 0.00143569994886136, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.5103464666769333e-16, + "reduced_integral": 0.001435699948861357, + "reduced_abs_error": 2.8189256484623115e-18, + "reduced_rel_error": 1.9634504066800133e-15 + } + }, + { + "exponents": [ + 2, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.02801645567318039, + "original_integral": 0.028016455673180386, + "original_abs_error": 3.469446951953614e-18, + "original_rel_error": 1.2383604094770806e-16, + "reduced_integral": 0.0280164556731804, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 3.7150812284312416e-16 + } + }, + { + "exponents": [ + 2, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.013223370305591857, + "original_integral": 0.013223370305591873, + "original_abs_error": 1.5612511283791264e-17, + "original_rel_error": 1.1806756464491579e-15, + "reduced_integral": 0.013223370305591862, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 3.9355854881638594e-16 + } + }, + { + "exponents": [ + 2, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.006824919673261954, + "original_integral": 0.006824919673261962, + "original_abs_error": 8.673617379884035e-18, + "original_rel_error": 1.270874646900936e-15, + "reduced_integral": 0.006824919673261954, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 2, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0037722391932440667, + "original_integral": 0.003772239193244071, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 1.149664288974324e-15, + "reduced_integral": 0.003772239193244068, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 3.4489928669229722e-16 + } + }, + { + "exponents": [ + 2, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0021924090519242294, + "original_integral": 0.0021924090519242294, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0021924090519242307, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 5.934305944598745e-16 + } + }, + { + "exponents": [ + 2, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0013215906013353603, + "original_integral": 0.0013215906013353607, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 3.281506909598195e-16, + "reduced_integral": 0.0013215906013353622, + "reduced_abs_error": 1.951563910473908e-18, + "reduced_rel_error": 1.4766781093191875e-15 + } + }, + { + "exponents": [ + 2, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.012831563252858731, + "original_integral": 0.01283156325285873, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.351919046644866e-16, + "reduced_integral": 0.012831563252858726, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 4.055757139934598e-16 + } + }, + { + "exponents": [ + 2, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.00605631613332888, + "original_integral": 0.006056316133328876, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 7.160803026902547e-16, + "reduced_integral": 0.006056316133328877, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 4.2964818161415286e-16 + } + }, + { + "exponents": [ + 2, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0031258196791457077, + "original_integral": 0.003125819679145709, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 4.1622445967138537e-16, + "reduced_integral": 0.0031258196791457073, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 1.3874148655712844e-16 + } + }, + { + "exponents": [ + 2, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0017276891259075272, + "original_integral": 0.0017276891259075291, + "original_abs_error": 1.951563910473908e-18, + "original_rel_error": 1.1295804790394702e-15, + "reduced_integral": 0.001727689125907528, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 5.020357684619867e-16 + } + }, + { + "exponents": [ + 2, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.001004125423789278, + "original_integral": 0.0010041254237892787, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 6.478486532453526e-16, + "reduced_integral": 0.0010041254237892782, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 2.1594955108178418e-16 + } + }, + { + "exponents": [ + 2, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.006460544291672678, + "original_integral": 0.006460544291672669, + "original_abs_error": 8.673617379884035e-18, + "original_rel_error": 1.3425521114473126e-15, + "reduced_integral": 0.006460544291672675, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 4.0276563343419377e-16 + } + }, + { + "exponents": [ + 2, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0030492854107254594, + "original_integral": 0.003049285410725459, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.4222377068043107e-16, + "reduced_integral": 0.003049285410725456, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 1.1377901654434486e-15 + } + }, + { + "exponents": [ + 2, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0015738142022877699, + "original_integral": 0.0015738142022877703, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.7556039865683194e-16, + "reduced_integral": 0.0015738142022877699, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 2, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0008698715737289538, + "original_integral": 0.0008698715737289542, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 4.9855735270794565e-16, + "reduced_integral": 0.0008698715737289536, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 2.4927867635397283e-16 + } + }, + { + "exponents": [ + 2, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0034935838778848544, + "original_integral": 0.003493583877884855, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.241363837689703e-16, + "reduced_integral": 0.0034935838778848514, + "reduced_abs_error": 3.0357660829594124e-18, + "reduced_rel_error": 8.689546863827921e-16 + } + }, + { + "exponents": [ + 2, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.001648922113839706, + "original_integral": 0.0016489221138397056, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.630087045071678e-16, + "reduced_integral": 0.001648922113839705, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 6.575217612679195e-16 + } + }, + { + "exponents": [ + 2, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0008510508829706097, + "original_integral": 0.0008510508829706093, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 5.09582773100981e-16, + "reduced_integral": 0.0008510508829706087, + "reduced_abs_error": 9.75781955236954e-19, + "reduced_rel_error": 1.1465612394772073e-15 + } + }, + { + "exponents": [ + 2, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.001989256841591169, + "original_integral": 0.0019892568415911713, + "original_abs_error": 2.168404344971009e-18, + "original_rel_error": 1.0900575026986173e-15, + "reduced_integral": 0.0019892568415911674, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 8.720460021588939e-16 + } + }, + { + "exponents": [ + 2, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0009389010571552447, + "original_integral": 0.0009389010571552455, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 8.08329605080383e-16, + "reduced_integral": 0.0009389010571552432, + "reduced_abs_error": 1.5178830414797062e-18, + "reduced_rel_error": 1.616659210160766e-15 + } + }, + { + "exponents": [ + 2, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0011754810852853989, + "original_integral": 0.0011754810852853995, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 5.534085674661115e-16, + "reduced_integral": 0.001175481085285397, + "reduced_abs_error": 1.951563910473908e-18, + "reduced_rel_error": 1.6602257023983345e-15 + } + }, + { + "exponents": [ + 3, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.01498059689723164, + "original_integral": 0.014980596897231625, + "original_abs_error": 1.5612511283791264e-17, + "original_rel_error": 1.0421821901286456e-15, + "reduced_integral": 0.01498059689723165, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 6.947881267524304e-16 + } + }, + { + "exponents": [ + 3, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.007070629578620323, + "original_integral": 0.007070629578620324, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.2267107594082873e-16, + "reduced_integral": 0.007070629578620325, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 3.6801322782248624e-16 + } + }, + { + "exponents": [ + 3, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0036493327947616466, + "original_integral": 0.0036493327947616453, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 3.5651519884680235e-16, + "reduced_integral": 0.0036493327947616483, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 4.753535984624031e-16 + } + }, + { + "exponents": [ + 3, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.002017042962647982, + "original_integral": 0.0020170429626479823, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.150082457464683e-16, + "reduced_integral": 0.0020170429626479845, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 1.2900494744788099e-15 + } + }, + { + "exponents": [ + 3, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0011722966182392302, + "original_integral": 0.0011722966182392306, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 3.699412437490293e-16, + "reduced_integral": 0.001172296618239232, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 1.4797649749961171e-15 + } + }, + { + "exponents": [ + 3, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.006861127577833471, + "original_integral": 0.006861127577833473, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.528335840279738e-16, + "reduced_integral": 0.006861127577833467, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 6.320839600699345e-16 + } + }, + { + "exponents": [ + 3, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.003238355048688473, + "original_integral": 0.0032383550486884717, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 4.0176033431217643e-16, + "reduced_integral": 0.003238355048688472, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 2.678402228747843e-16 + } + }, + { + "exponents": [ + 3, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0016713978789095016, + "original_integral": 0.0016713978789095034, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.0378878050920001e-15, + "reduced_integral": 0.0016713978789095023, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 3.892079269095e-16 + } + }, + { + "exponents": [ + 3, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0009238075886853635, + "original_integral": 0.0009238075886853645, + "original_abs_error": 9.75781955236954e-19, + "original_rel_error": 1.0562610300977862e-15, + "reduced_integral": 0.000923807588685364, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 5.86811683387659e-16 + } + }, + { + "exponents": [ + 3, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.003454498702450346, + "original_integral": 0.00345449870245035, + "original_abs_error": 3.903127820947816e-18, + "original_rel_error": 1.1298680813454201e-15, + "reduced_integral": 0.0034544987024503443, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 5.021635917090756e-16 + } + }, + { + "exponents": [ + 3, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0016304744645632033, + "original_integral": 0.0016304744645632041, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 5.319689187654747e-16, + "reduced_integral": 0.0016304744645632028, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 2.6598445938273737e-16 + } + }, + { + "exponents": [ + 3, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0008415295792815345, + "original_integral": 0.0008415295792815349, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 5.1534833673281195e-16, + "reduced_integral": 0.0008415295792815345, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 3, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0018680439957064418, + "original_integral": 0.001868043995706441, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 4.643154764994664e-16, + "reduced_integral": 0.001868043995706441, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 4.643154764994664e-16 + } + }, + { + "exponents": [ + 3, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0008816903105274064, + "original_integral": 0.000881690310527407, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 7.378115600501225e-16, + "reduced_integral": 0.0008816903105274057, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 7.378115600501225e-16 + } + }, + { + "exponents": [ + 3, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0010636696952878546, + "original_integral": 0.0010636696952878544, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 2.0386068669411387e-16, + "reduced_integral": 0.0010636696952878541, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 4.0772137338822774e-16 + } + }, + { + "exponents": [ + 4, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.008440467836218837, + "original_integral": 0.008440467836218851, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 1.6441965157741104e-15, + "reduced_integral": 0.008440467836218846, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 1.027622822358819e-15 + } + }, + { + "exponents": [ + 4, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.003983781283854647, + "original_integral": 0.003983781283854649, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 4.3544646464584893e-16, + "reduced_integral": 0.003983781283854651, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 1.0886161616146224e-15 + } + }, + { + "exponents": [ + 4, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0020561314271487006, + "original_integral": 0.0020561314271487037, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 1.4764455437409468e-15, + "reduced_integral": 0.0020561314271487037, + "reduced_abs_error": 3.0357660829594124e-18, + "reduced_rel_error": 1.4764455437409468e-15 + } + }, + { + "exponents": [ + 4, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0011364558012803866, + "original_integral": 0.0011364558012803869, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.9080410716615452e-16, + "reduced_integral": 0.0011364558012803882, + "reduced_abs_error": 1.5178830414797062e-18, + "reduced_rel_error": 1.3356287501630816e-15 + } + }, + { + "exponents": [ + 4, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.003865742269027963, + "original_integral": 0.0038657422690279637, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 2.243713309440313e-16, + "reduced_integral": 0.0038657422690279645, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 4.487426618880626e-16 + } + }, + { + "exponents": [ + 4, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.001824575603910886, + "original_integral": 0.0018245756039108856, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.3768862636584016e-16, + "reduced_integral": 0.001824575603910887, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 4.753772527316803e-16 + } + }, + { + "exponents": [ + 4, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0009417101424755005, + "original_integral": 0.0009417101424755011, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 6.90787190399435e-16, + "reduced_integral": 0.0009417101424755012, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 8.059183887993409e-16 + } + }, + { + "exponents": [ + 4, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.001946356703161814, + "original_integral": 0.0019463567031618122, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 8.91266987782243e-16, + "reduced_integral": 0.0019463567031618126, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 6.684502408366822e-16 + } + }, + { + "exponents": [ + 4, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0009186527993731023, + "original_integral": 0.0009186527993731027, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 4.720835437394301e-16, + "reduced_integral": 0.0009186527993731023, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 4, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0010525058093857175, + "original_integral": 0.0010525058093857169, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 6.180690858808387e-16, + "reduced_integral": 0.0010525058093857169, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 6.180690858808387e-16 + } + }, + { + "exponents": [ + 5, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0049366547789406945, + "original_integral": 0.004936654778940692, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 5.270948305045477e-16, + "reduced_integral": 0.004936654778940703, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 1.7569827683484922e-15 + } + }, + { + "exponents": [ + 5, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.00233003114220808, + "original_integral": 0.002330031142208081, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 3.7225328120139994e-16, + "reduced_integral": 0.002330031142208084, + "reduced_abs_error": 3.903127820947816e-18, + "reduced_rel_error": 1.6751397654062997e-15 + } + }, + { + "exponents": [ + 5, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0012025886755242907, + "original_integral": 0.0012025886755242922, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 1.2621797231027118e-15, + "reduced_integral": 0.001202588675524293, + "reduced_abs_error": 2.3852447794681098e-18, + "reduced_rel_error": 1.9834252791614042e-15 + } + }, + { + "exponents": [ + 5, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0022609925678123447, + "original_integral": 0.0022609925678123474, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 1.1508596936622816e-15, + "reduced_integral": 0.002260992567812346, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 5.754298468311408e-16 + } + }, + { + "exponents": [ + 5, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0010671564715801782, + "original_integral": 0.0010671564715801784, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 2.0319460198373458e-16, + "reduced_integral": 0.00106715647158018, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 1.6255568158698766e-15 + } + }, + { + "exponents": [ + 5, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0011383837136320907, + "original_integral": 0.001138383713632092, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 1.1428858225945102e-15, + "reduced_integral": 0.0011383837136320913, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 5.714429112972551e-16 + } + }, + { + "exponents": [ + 6, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0029669160778858826, + "original_integral": 0.00296691607788588, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 8.770336422253516e-16, + "reduced_integral": 0.002966916077885886, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 1.1693781896338021e-15 + } + }, + { + "exponents": [ + 6, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0014003423709679262, + "original_integral": 0.0014003423709679273, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 7.742407820853813e-16, + "reduced_integral": 0.0014003423709679308, + "reduced_abs_error": 4.553649124439119e-18, + "reduced_rel_error": 3.2518112847586015e-15 + } + }, + { + "exponents": [ + 6, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0013588503757725524, + "original_integral": 0.0013588503757725515, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 6.383055511135867e-16, + "reduced_integral": 0.001358850375772554, + "reduced_abs_error": 1.5178830414797062e-18, + "reduced_rel_error": 1.1170347144487768e-15 + } + }, + { + "exponents": [ + 7, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 7, + "original_points": 486, + "reduced_points": 120, + "exact_integral": 0.0018197428512164628, + "original_integral": 0.001819742851216463, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.1915993204872175e-16, + "reduced_integral": 0.0018197428512164672, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 2.383198640974435e-15 + } + } + ] + }, + { + "strength": 8, + "results": [ + { + "exponents": [ + 0, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.12499999999999997, + "original_integral": 0.125, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 2.2204460492503136e-16, + "reduced_integral": 0.1249999999999998, + "reduced_abs_error": 1.6653345369377348e-16, + "reduced_rel_error": 1.3322676295501882e-15 + } + }, + { + "exponents": [ + 0, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.05899822973615082, + "original_integral": 0.058998229736150876, + "original_abs_error": 5.551115123125783e-17, + "original_rel_error": 9.408952010850538e-16, + "reduced_integral": 0.058998229736150744, + "reduced_abs_error": 7.632783294297951e-17, + "reduced_rel_error": 1.293730901491949e-15 + } + }, + { + "exponents": [ + 0, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.030450495562663715, + "original_integral": 0.030450495562663757, + "original_abs_error": 4.163336342344337e-17, + "original_rel_error": 1.367247483305044e-15, + "reduced_integral": 0.03045049556266368, + "reduced_abs_error": 3.469446951953614e-17, + "reduced_rel_error": 1.1393729027542032e-15 + } + }, + { + "exponents": [ + 0, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.016830462234625018, + "original_integral": 0.016830462234625046, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 1.649127351863685e-15, + "reduced_integral": 0.016830462234625004, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 8.245636759318425e-16 + } + }, + { + "exponents": [ + 0, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.00978179162587195, + "original_integral": 0.00978179162587195, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.009781791625871944, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 5.320263022334133e-16 + } + }, + { + "exponents": [ + 0, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0058964926575299035, + "original_integral": 0.005896492657529905, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.941958171967773e-16, + "reduced_integral": 0.005896492657529899, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 7.354895429919433e-16 + } + }, + { + "exponents": [ + 0, + 0, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0036511518202430055, + "original_integral": 0.0036511518202430085, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 8.314543553429571e-16, + "reduced_integral": 0.003651151820243004, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 3.563375808612673e-16 + } + }, + { + "exponents": [ + 0, + 0, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.002306984964566927, + "original_integral": 0.0023069849645669313, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 1.879859971586826e-15, + "reduced_integral": 0.0023069849645669265, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 1.879859971586826e-16 + } + }, + { + "exponents": [ + 0, + 0, + 8 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.001480624539659955, + "original_integral": 0.001480624539659957, + "original_abs_error": 1.951563910473908e-18, + "original_rel_error": 1.3180680572281413e-15, + "reduced_integral": 0.001480624539659955, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 0, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.057250118477433484, + "original_integral": 0.05725011847743346, + "original_abs_error": 2.0816681711721685e-17, + "original_rel_error": 3.6360940842292024e-16, + "reduced_integral": 0.05725011847743341, + "reduced_abs_error": 7.632783294297951e-17, + "reduced_rel_error": 1.3332344975507075e-15 + } + }, + { + "exponents": [ + 0, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.027021245138827792, + "original_integral": 0.02702124513882782, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 1.027176041408467e-15, + "reduced_integral": 0.027021245138827768, + "reduced_abs_error": 2.42861286636753e-17, + "reduced_rel_error": 8.987790362324086e-16 + } + }, + { + "exponents": [ + 0, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.013946355829272483, + "original_integral": 0.01394635582927249, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 4.975417226443446e-16, + "reduced_integral": 0.013946355829272476, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 4.975417226443446e-16 + } + }, + { + "exponents": [ + 0, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0077083676556980165, + "original_integral": 0.0077083676556980165, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.007708367655698014, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 3.3756630848319127e-16 + } + }, + { + "exponents": [ + 0, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.004480069836021888, + "original_integral": 0.004480069836021894, + "original_abs_error": 6.071532165918825e-18, + "original_rel_error": 1.3552315897178265e-15, + "reduced_integral": 0.004480069836021888, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 0, + 1, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.002700599225959228, + "original_integral": 0.002700599225959227, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 3.211738082611368e-16, + "reduced_integral": 0.0027005992259592263, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 6.423476165222736e-16 + } + }, + { + "exponents": [ + 0, + 1, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0016722309943040716, + "original_integral": 0.001672230994304074, + "original_abs_error": 2.3852447794681098e-18, + "original_rel_error": 1.4263847444478037e-15, + "reduced_integral": 0.0016722309943040716, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 0, + 1, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.001056601300376914, + "original_integral": 0.001056601300376916, + "original_abs_error": 1.951563910473908e-18, + "original_rel_error": 1.8470201671886454e-15, + "reduced_integral": 0.0010566013003769142, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 2.052244630209606e-16 + } + }, + { + "exponents": [ + 0, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.02882477519210804, + "original_integral": 0.028824775192108076, + "original_abs_error": 3.469446951953614e-17, + "original_rel_error": 1.2036336550175477e-15, + "reduced_integral": 0.028824775192108024, + "reduced_abs_error": 1.734723475976807e-17, + "reduced_rel_error": 6.018168275087739e-16 + } + }, + { + "exponents": [ + 0, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.013604885671015126, + "original_integral": 0.013604885671015147, + "original_abs_error": 2.0816681711721685e-17, + "original_rel_error": 1.5300886913052942e-15, + "reduced_integral": 0.013604885671015123, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 2.550147818842157e-16 + } + }, + { + "exponents": [ + 0, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.007021829512656519, + "original_integral": 0.007021829512656529, + "original_abs_error": 9.540979117872439e-18, + "original_rel_error": 1.3587597221885365e-15, + "reduced_integral": 0.007021829512656514, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 7.411416666482927e-16 + } + }, + { + "exponents": [ + 0, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.003881074322338644, + "original_integral": 0.003881074322338643, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 2.234849595628851e-16, + "reduced_integral": 0.0038810743223386423, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 4.469699191257702e-16 + } + }, + { + "exponents": [ + 0, + 2, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0022556635567344318, + "original_integral": 0.002255663556734433, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 5.767893013558059e-16, + "reduced_integral": 0.0022556635567344313, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 1.9226310045193533e-16 + } + }, + { + "exponents": [ + 0, + 2, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0013597206022017202, + "original_integral": 0.001359720602201722, + "original_abs_error": 1.951563910473908e-18, + "original_rel_error": 1.4352683244733137e-15, + "reduced_integral": 0.0013597206022017195, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 4.784227748244379e-16 + } + }, + { + "exponents": [ + 0, + 2, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0008419490432860857, + "original_integral": 0.000841949043286086, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 3.8631869035229855e-16, + "reduced_integral": 0.0008419490432860853, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 5.15091587136398e-16 + } + }, + { + "exponents": [ + 0, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.015587195961894966, + "original_integral": 0.015587195961894949, + "original_abs_error": 1.734723475976807e-17, + "original_rel_error": 1.1129156778535254e-15, + "reduced_integral": 0.015587195961894949, + "reduced_abs_error": 1.734723475976807e-17, + "reduced_rel_error": 1.1129156778535254e-15 + } + }, + { + "exponents": [ + 0, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.007356935746418251, + "original_integral": 0.0073569357464182555, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 5.894857369188534e-16, + "reduced_integral": 0.007356935746418244, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 9.431771790701655e-16 + } + }, + { + "exponents": [ + 0, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0037971027317764182, + "original_integral": 0.003797102731776424, + "original_abs_error": 5.637851296924623e-18, + "original_rel_error": 1.4847771301375978e-15, + "reduced_integral": 0.0037971027317764143, + "reduced_abs_error": 3.903127820947816e-18, + "reduced_rel_error": 1.0279226285567986e-15 + } + }, + { + "exponents": [ + 0, + 3, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.002098717703842982, + "original_integral": 0.0020987177038429843, + "original_abs_error": 2.168404344971009e-18, + "original_rel_error": 1.0332043899951017e-15, + "reduced_integral": 0.00209871770384298, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 1.0332043899951017e-15 + } + }, + { + "exponents": [ + 0, + 3, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.001219765623447114, + "original_integral": 0.001219765623447115, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 7.110888529037235e-16, + "reduced_integral": 0.0012197656234471134, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 5.333166396777927e-16 + } + }, + { + "exponents": [ + 0, + 3, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0007352782923263471, + "original_integral": 0.0007352782923263479, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.1796373523338458e-15, + "reduced_integral": 0.0007352782923263461, + "reduced_abs_error": 9.75781955236954e-19, + "reduced_rel_error": 1.3270920213755765e-15 + } + }, + { + "exponents": [ + 0, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.008875394807235764, + "original_integral": 0.008875394807235766, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.9545310531567051e-16, + "reduced_integral": 0.008875394807235752, + "reduced_abs_error": 1.214306433183765e-17, + "reduced_rel_error": 1.3681717372096937e-15 + } + }, + { + "exponents": [ + 0, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.004189060654690686, + "original_integral": 0.004189060654690683, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 6.211619808014799e-16, + "reduced_integral": 0.004189060654690681, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 1.2423239616029598e-15 + } + }, + { + "exponents": [ + 0, + 4, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.00216208136155697, + "original_integral": 0.0021620813615569707, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 4.01169795647188e-16, + "reduced_integral": 0.0021620813615569673, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 1.203509386941564e-15 + } + }, + { + "exponents": [ + 0, + 4, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0011950159769645481, + "original_integral": 0.0011950159769645499, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.4516320362370101e-15, + "reduced_integral": 0.0011950159769645462, + "reduced_abs_error": 1.951563910473908e-18, + "reduced_rel_error": 1.6330860407666365e-15 + } + }, + { + "exponents": [ + 0, + 4, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0006945381008138094, + "original_integral": 0.0006945381008138092, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 3.1220811967410134e-16, + "reduced_integral": 0.0006945381008138081, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 1.8732487180446083e-15 + } + }, + { + "exponents": [ + 0, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.005244601150649224, + "original_integral": 0.005244601150649229, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 9.922909823726447e-16, + "reduced_integral": 0.0052446011506492194, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 8.269091519772039e-16 + } + }, + { + "exponents": [ + 0, + 5, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.002475377468483872, + "original_integral": 0.0024753774684838697, + "original_abs_error": 2.168404344971009e-18, + "original_rel_error": 8.759893683201055e-16, + "reduced_integral": 0.0024753774684838662, + "reduced_abs_error": 5.637851296924623e-18, + "reduced_rel_error": 2.2775723576322742e-15 + } + }, + { + "exponents": [ + 0, + 5, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.001277605632526282, + "original_integral": 0.001277605632526281, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 8.486203761810678e-16, + "reduced_integral": 0.0012776056325262792, + "reduced_abs_error": 2.8189256484623115e-18, + "reduced_rel_error": 2.206412978070776e-15 + } + }, + { + "exponents": [ + 0, + 5, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0007061524928133816, + "original_integral": 0.0007061524928133826, + "original_abs_error": 9.75781955236954e-19, + "original_rel_error": 1.3818289465343978e-15, + "reduced_integral": 0.0007061524928133799, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.4565847938389294e-15 + } + }, + { + "exponents": [ + 0, + 6, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0031841740489256596, + "original_integral": 0.0031841740489256583, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 4.0859657386554515e-16, + "reduced_integral": 0.0031841740489256557, + "reduced_abs_error": 3.903127820947816e-18, + "reduced_rel_error": 1.2257897215966355e-15 + } + }, + { + "exponents": [ + 0, + 6, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0015028850564672448, + "original_integral": 0.0015028850564672465, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.1542622428188441e-15, + "reduced_integral": 0.001502885056467242, + "reduced_abs_error": 2.8189256484623115e-18, + "reduced_rel_error": 1.8756761445806215e-15 + } + }, + { + "exponents": [ + 0, + 6, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0007756774219804779, + "original_integral": 0.0007756774219804781, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 2.795497565772364e-16, + "reduced_integral": 0.0007756774219804761, + "reduced_abs_error": 1.8431436932253575e-18, + "reduced_rel_error": 2.3761729309065096e-15 + } + }, + { + "exponents": [ + 0, + 7, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.001972861658829849, + "original_integral": 0.0019728616588298494, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.198232537254681e-16, + "reduced_integral": 0.001972861658829847, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 1.0991162686273405e-15 + } + }, + { + "exponents": [ + 0, + 7, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0009311627630822963, + "original_integral": 0.0009311627630822967, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 4.657412067882197e-16, + "reduced_integral": 0.0009311627630822942, + "reduced_abs_error": 2.0599841277224584e-18, + "reduced_rel_error": 2.2122707322440436e-15 + } + }, + { + "exponents": [ + 0, + 8, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0012416379281119064, + "original_integral": 0.001241637928111907, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 5.239219008721135e-16, + "reduced_integral": 0.0012416379281119036, + "reduced_abs_error": 2.8189256484623115e-18, + "reduced_rel_error": 2.270328237112492e-15 + } + }, + { + "exponents": [ + 1, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.056360767612003086, + "original_integral": 0.05636076761200307, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 2.4623134843285776e-16, + "reduced_integral": 0.05636076761200298, + "reduced_abs_error": 1.0408340855860843e-16, + "reduced_rel_error": 1.8467351132464334e-15 + } + }, + { + "exponents": [ + 1, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.02660148412543013, + "original_integral": 0.026601484125430137, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 2.6084611938150766e-16, + "reduced_integral": 0.026601484125430102, + "reduced_abs_error": 2.7755575615628914e-17, + "reduced_rel_error": 1.0433844775260306e-15 + } + }, + { + "exponents": [ + 1, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.013729706432620965, + "original_integral": 0.013729706432620954, + "original_abs_error": 1.0408340855860843e-17, + "original_rel_error": 7.580891045952188e-16, + "reduced_integral": 0.013729706432620956, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 6.317409204960156e-16 + } + }, + { + "exponents": [ + 1, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.007588622166466357, + "original_integral": 0.007588622166466358, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.1429765759339295e-16, + "reduced_integral": 0.007588622166466354, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 3.428929727801789e-16 + } + }, + { + "exponents": [ + 1, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.004410474277238454, + "original_integral": 0.004410474277238458, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 9.832975814695012e-16, + "reduced_integral": 0.004410474277238456, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 5.899785488817007e-16 + } + }, + { + "exponents": [ + 1, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0026586468191754017, + "original_integral": 0.0026586468191754043, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 9.78725415951362e-16, + "reduced_integral": 0.002658646819175401, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 3.262418053171207e-16 + } + }, + { + "exponents": [ + 1, + 0, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0016462537540548646, + "original_integral": 0.0016462537540548675, + "original_abs_error": 2.8189256484623115e-18, + "original_rel_error": 1.7123275445957557e-15, + "reduced_integral": 0.0016462537540548646, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 1, + 0, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.001040187547778734, + "original_integral": 0.0010401875477787355, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 1.459239773366895e-15, + "reduced_integral": 0.0010401875477787348, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 8.338512990667973e-16 + } + }, + { + "exponents": [ + 1, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.02581328498613018, + "original_integral": 0.025813284986130173, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 2.6881095945888283e-16, + "reduced_integral": 0.02581328498613016, + "reduced_abs_error": 2.0816681711721685e-17, + "reduced_rel_error": 8.064328783766484e-16 + } + }, + { + "exponents": [ + 1, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.01218350494285153, + "original_integral": 0.012183504942851536, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 4.271488748386713e-16, + "reduced_integral": 0.01218350494285152, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 8.542977496773426e-16 + } + }, + { + "exponents": [ + 1, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.006288218559423448, + "original_integral": 0.006288218559423449, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.758688266929227e-16, + "reduced_integral": 0.006288218559423446, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.758688266929227e-16 + } + }, + { + "exponents": [ + 1, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0034755961448854167, + "original_integral": 0.003475596144885417, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.2477884394951743e-16, + "reduced_integral": 0.0034755961448854145, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 6.238942197475872e-16 + } + }, + { + "exponents": [ + 1, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.002020001399308595, + "original_integral": 0.002020001399308598, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 1.502853455447354e-15, + "reduced_integral": 0.002020001399308596, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 6.440800523345803e-16 + } + }, + { + "exponents": [ + 1, + 1, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0012176627630995475, + "original_integral": 0.0012176627630995486, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 8.903960976236797e-16, + "reduced_integral": 0.0012176627630995477, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 1.7807921952473598e-16 + } + }, + { + "exponents": [ + 1, + 1, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0007539857797084852, + "original_integral": 0.0007539857797084867, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 2.013145449595307e-15, + "reduced_integral": 0.0007539857797084858, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 8.627766212551314e-16 + } + }, + { + "exponents": [ + 1, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.012996691648565066, + "original_integral": 0.012996691648565059, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 5.338969402011884e-16, + "reduced_integral": 0.012996691648565054, + "reduced_abs_error": 1.214306433183765e-17, + "reduced_rel_error": 9.343196453520799e-16 + } + }, + { + "exponents": [ + 1, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.006134254397535635, + "original_integral": 0.006134254397535638, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 4.241893220161472e-16, + "reduced_integral": 0.006134254397535627, + "reduced_abs_error": 7.806255641895632e-18, + "reduced_rel_error": 1.2725679660484416e-15 + } + }, + { + "exponents": [ + 1, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.003166045610991512, + "original_integral": 0.0031660456109915158, + "original_abs_error": 3.903127820947816e-18, + "original_rel_error": 1.2328084622019933e-15, + "reduced_integral": 0.0031660456109915093, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 8.218723081346622e-16 + } + }, + { + "exponents": [ + 1, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.001749922623729925, + "original_integral": 0.0017499226237299257, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 3.717428957542874e-16, + "reduced_integral": 0.0017499226237299252, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 1.2391429858476247e-16 + } + }, + { + "exponents": [ + 1, + 2, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0010170474362557894, + "original_integral": 0.001017047436255791, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 1.4924407528794515e-15, + "reduced_integral": 0.0010170474362557892, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 2.1320582183992163e-16 + } + }, + { + "exponents": [ + 1, + 2, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0006130791750235527, + "original_integral": 0.0006130791750235534, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 1.0610722562323399e-15, + "reduced_integral": 0.0006130791750235527, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 1, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.007028050634648921, + "original_integral": 0.007028050634648921, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.00702805063464891, + "reduced_abs_error": 1.1275702593849246e-17, + "reduced_rel_error": 1.6043855088719792e-15 + } + }, + { + "exponents": [ + 1, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.003317140367522541, + "original_integral": 0.0033171403675225425, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 3.9221813454771275e-16, + "reduced_integral": 0.0033171403675225372, + "reduced_abs_error": 3.903127820947816e-18, + "reduced_rel_error": 1.1766544036431382e-15 + } + }, + { + "exponents": [ + 1, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0017120609973164226, + "original_integral": 0.0017120609973164224, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.2665461968760946e-16, + "reduced_integral": 0.001712060997316422, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 3.799638590628284e-16 + } + }, + { + "exponents": [ + 1, + 3, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.000946282726315928, + "original_integral": 0.0009462827263159287, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 8.020240670508353e-16, + "reduced_integral": 0.0009462827263159273, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 6.874492003292874e-16 + } + }, + { + "exponents": [ + 1, + 3, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0005499754147537028, + "original_integral": 0.0005499754147537029, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 1.9713647981356513e-16, + "reduced_integral": 0.0005499754147537029, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 1.9713647981356513e-16 + } + }, + { + "exponents": [ + 1, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.004001792513563149, + "original_integral": 0.004001792513563149, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.004001792513563145, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 1.0837165283415892e-15 + } + }, + { + "exponents": [ + 1, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0018887893925728578, + "original_integral": 0.0018887893925728573, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.29607848656675e-16, + "reduced_integral": 0.0018887893925728554, + "reduced_abs_error": 2.3852447794681098e-18, + "reduced_rel_error": 1.2628431676117124e-15 + } + }, + { + "exponents": [ + 1, + 4, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0009748525214156445, + "original_integral": 0.0009748525214156456, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 1.1121704551895363e-15, + "reduced_integral": 0.0009748525214156437, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 7.785193186326754e-16 + } + }, + { + "exponents": [ + 1, + 4, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0005388161421626378, + "original_integral": 0.0005388161421626381, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 6.036579573138953e-16, + "reduced_integral": 0.0005388161421626375, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 6.036579573138953e-16 + } + }, + { + "exponents": [ + 1, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0023647179733550794, + "original_integral": 0.0023647179733550763, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 1.2837751127895581e-15, + "reduced_integral": 0.002364717973355075, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 1.833964446842226e-15 + } + }, + { + "exponents": [ + 1, + 5, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0011161133940256634, + "original_integral": 0.0011161133940256634, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.001116113394025662, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 1.1656903446789833e-15 + } + }, + { + "exponents": [ + 1, + 5, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0005760546732367999, + "original_integral": 0.0005760546732368001, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 3.7642335800991564e-16, + "reduced_integral": 0.000576054673236799, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 1.5056934320396626e-15 + } + }, + { + "exponents": [ + 1, + 6, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0014356999488613598, + "original_integral": 0.00143569994886136, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.5103464666769333e-16, + "reduced_integral": 0.0014356999488613566, + "reduced_abs_error": 3.2526065174565133e-18, + "reduced_rel_error": 2.2655197000154e-15 + } + }, + { + "exponents": [ + 1, + 6, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0006776300433208199, + "original_integral": 0.0006776300433208202, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 3.1999825957309135e-16, + "reduced_integral": 0.000677630043320819, + "reduced_abs_error": 9.75781955236954e-19, + "reduced_rel_error": 1.439992168078911e-15 + } + }, + { + "exponents": [ + 1, + 7, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0008895359798715205, + "original_integral": 0.0008895359798715203, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 2.4376803120253786e-16, + "reduced_integral": 0.0008895359798715186, + "reduced_abs_error": 1.951563910473908e-18, + "reduced_rel_error": 2.1939122808228405e-15 + } + }, + { + "exponents": [ + 2, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.02801645567318039, + "original_integral": 0.028016455673180386, + "original_abs_error": 3.469446951953614e-18, + "original_rel_error": 1.2383604094770806e-16, + "reduced_integral": 0.028016455673180354, + "reduced_abs_error": 3.469446951953614e-17, + "reduced_rel_error": 1.2383604094770805e-15 + } + }, + { + "exponents": [ + 2, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.013223370305591857, + "original_integral": 0.013223370305591873, + "original_abs_error": 1.5612511283791264e-17, + "original_rel_error": 1.1806756464491579e-15, + "reduced_integral": 0.01322337030559185, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 5.247447317551813e-16 + } + }, + { + "exponents": [ + 2, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.006824919673261954, + "original_integral": 0.006824919673261962, + "original_abs_error": 8.673617379884035e-18, + "original_rel_error": 1.270874646900936e-15, + "reduced_integral": 0.006824919673261952, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.5417492938018715e-16 + } + }, + { + "exponents": [ + 2, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0037722391932440667, + "original_integral": 0.003772239193244071, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 1.149664288974324e-15, + "reduced_integral": 0.0037722391932440667, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 2, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0021924090519242294, + "original_integral": 0.0021924090519242294, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0021924090519242294, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 2, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0013215906013353603, + "original_integral": 0.0013215906013353607, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 3.281506909598195e-16, + "reduced_integral": 0.0013215906013353616, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 9.844520728794583e-16 + } + }, + { + "exponents": [ + 2, + 0, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0008183386650231205, + "original_integral": 0.0008183386650231222, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.1198112103474863e-15, + "reduced_integral": 0.0008183386650231215, + "reduced_abs_error": 9.75781955236954e-19, + "reduced_rel_error": 1.192393805820461e-15 + } + }, + { + "exponents": [ + 2, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.012831563252858731, + "original_integral": 0.01283156325285873, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.351919046644866e-16, + "reduced_integral": 0.01283156325285871, + "reduced_abs_error": 2.0816681711721685e-17, + "reduced_rel_error": 1.6223028559738392e-15 + } + }, + { + "exponents": [ + 2, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.00605631613332888, + "original_integral": 0.006056316133328876, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 7.160803026902547e-16, + "reduced_integral": 0.006056316133328874, + "reduced_abs_error": 6.071532165918825e-18, + "reduced_rel_error": 1.0025124237663566e-15 + } + }, + { + "exponents": [ + 2, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0031258196791457077, + "original_integral": 0.003125819679145709, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 4.1622445967138537e-16, + "reduced_integral": 0.0031258196791457086, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 2.774829731142569e-16 + } + }, + { + "exponents": [ + 2, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0017276891259075272, + "original_integral": 0.0017276891259075291, + "original_abs_error": 1.951563910473908e-18, + "original_rel_error": 1.1295804790394702e-15, + "reduced_integral": 0.0017276891259075276, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 2.5101788423099336e-16 + } + }, + { + "exponents": [ + 2, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.001004125423789278, + "original_integral": 0.0010041254237892787, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 6.478486532453526e-16, + "reduced_integral": 0.0010041254237892784, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 4.3189910216356837e-16 + } + }, + { + "exponents": [ + 2, + 1, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0006052897480408958, + "original_integral": 0.0006052897480408965, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 1.2538482985979403e-15, + "reduced_integral": 0.0006052897480408967, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 1.4329694841119317e-15 + } + }, + { + "exponents": [ + 2, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.006460544291672678, + "original_integral": 0.006460544291672669, + "original_abs_error": 8.673617379884035e-18, + "original_rel_error": 1.3425521114473126e-15, + "reduced_integral": 0.006460544291672668, + "reduced_abs_error": 9.540979117872439e-18, + "reduced_rel_error": 1.4768073225920438e-15 + } + }, + { + "exponents": [ + 2, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0030492854107254594, + "original_integral": 0.003049285410725459, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.4222377068043107e-16, + "reduced_integral": 0.003049285410725459, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 1.4222377068043107e-16 + } + }, + { + "exponents": [ + 2, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0015738142022877699, + "original_integral": 0.0015738142022877703, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.7556039865683194e-16, + "reduced_integral": 0.0015738142022877696, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 1.3778019932841597e-16 + } + }, + { + "exponents": [ + 2, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0008698715737289538, + "original_integral": 0.0008698715737289542, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 4.9855735270794565e-16, + "reduced_integral": 0.0008698715737289546, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 8.724753672389049e-16 + } + }, + { + "exponents": [ + 2, + 2, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.000505565584406869, + "original_integral": 0.0005055655844068694, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 8.578132736289743e-16, + "reduced_integral": 0.0005055655844068695, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 1.0722665920362178e-15 + } + }, + { + "exponents": [ + 2, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0034935838778848544, + "original_integral": 0.003493583877884855, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.241363837689703e-16, + "reduced_integral": 0.0034935838778848488, + "reduced_abs_error": 5.637851296924623e-18, + "reduced_rel_error": 1.613772988996614e-15 + } + }, + { + "exponents": [ + 2, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.001648922113839706, + "original_integral": 0.0016489221138397056, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.630087045071678e-16, + "reduced_integral": 0.0016489221138397065, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 2.630087045071678e-16 + } + }, + { + "exponents": [ + 2, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0008510508829706097, + "original_integral": 0.0008510508829706093, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 5.09582773100981e-16, + "reduced_integral": 0.0008510508829706095, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 2.547913865504905e-16 + } + }, + { + "exponents": [ + 2, + 3, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0004703890521618867, + "original_integral": 0.00047038905216188715, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 9.21962080114374e-16, + "reduced_integral": 0.0004703890521618869, + "reduced_abs_error": 1.6263032587282567e-19, + "reduced_rel_error": 3.4573578004289016e-16 + } + }, + { + "exponents": [ + 2, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.001989256841591169, + "original_integral": 0.0019892568415911713, + "original_abs_error": 2.168404344971009e-18, + "original_rel_error": 1.0900575026986173e-15, + "reduced_integral": 0.001989256841591165, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 2.1801150053972346e-15 + } + }, + { + "exponents": [ + 2, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0009389010571552447, + "original_integral": 0.0009389010571552455, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 8.08329605080383e-16, + "reduced_integral": 0.0009389010571552442, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 5.773782893431307e-16 + } + }, + { + "exponents": [ + 2, + 4, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.00048459085302296267, + "original_integral": 0.00048459085302296305, + "original_abs_error": 3.7947076036992655e-19, + "original_rel_error": 7.830745421683498e-16, + "reduced_integral": 0.00048459085302296245, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 4.474711669533427e-16 + } + }, + { + "exponents": [ + 2, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0011754810852853989, + "original_integral": 0.0011754810852853995, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 5.534085674661115e-16, + "reduced_integral": 0.0011754810852853965, + "reduced_abs_error": 2.3852447794681098e-18, + "reduced_rel_error": 2.029164747375742e-15 + } + }, + { + "exponents": [ + 2, + 5, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.000554810424961343, + "original_integral": 0.0005548104249613433, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 5.862554795510813e-16, + "reduced_integral": 0.0005548104249613426, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 7.81673972734775e-16 + } + }, + { + "exponents": [ + 2, + 6, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0007136741687793364, + "original_integral": 0.0007136741687793378, + "original_abs_error": 1.4094628242311558e-18, + "original_rel_error": 1.9749388248728292e-15, + "reduced_integral": 0.0007136741687793347, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.4306939383050205e-15 + } + }, + { + "exponents": [ + 3, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.01498059689723164, + "original_integral": 0.014980596897231625, + "original_abs_error": 1.5612511283791264e-17, + "original_rel_error": 1.0421821901286456e-15, + "reduced_integral": 0.014980596897231625, + "reduced_abs_error": 1.5612511283791264e-17, + "reduced_rel_error": 1.0421821901286456e-15 + } + }, + { + "exponents": [ + 3, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.007070629578620323, + "original_integral": 0.007070629578620324, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.2267107594082873e-16, + "reduced_integral": 0.007070629578620317, + "reduced_abs_error": 6.071532165918825e-18, + "reduced_rel_error": 8.586975315858012e-16 + } + }, + { + "exponents": [ + 3, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0036493327947616466, + "original_integral": 0.0036493327947616453, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 3.5651519884680235e-16, + "reduced_integral": 0.003649332794761645, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 4.753535984624031e-16 + } + }, + { + "exponents": [ + 3, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.002017042962647982, + "original_integral": 0.0020170429626479823, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.150082457464683e-16, + "reduced_integral": 0.0020170429626479823, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 2.150082457464683e-16 + } + }, + { + "exponents": [ + 3, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0011722966182392302, + "original_integral": 0.0011722966182392306, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 3.699412437490293e-16, + "reduced_integral": 0.0011722966182392304, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 1.8497062187451464e-16 + } + }, + { + "exponents": [ + 3, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0007066638368795324, + "original_integral": 0.0007066638368795335, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 1.534254501083706e-15, + "reduced_integral": 0.0007066638368795333, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 1.2274036008669648e-15 + } + }, + { + "exponents": [ + 3, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.006861127577833471, + "original_integral": 0.006861127577833473, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.528335840279738e-16, + "reduced_integral": 0.00686112757783346, + "reduced_abs_error": 1.1275702593849246e-17, + "reduced_rel_error": 1.6434182961818295e-15 + } + }, + { + "exponents": [ + 3, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.003238355048688473, + "original_integral": 0.0032383550486884717, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 4.0176033431217643e-16, + "reduced_integral": 0.0032383550486884704, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 8.035206686243529e-16 + } + }, + { + "exponents": [ + 3, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0016713978789095016, + "original_integral": 0.0016713978789095034, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.0378878050920001e-15, + "reduced_integral": 0.001671397878909502, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 2.5947195127300003e-16 + } + }, + { + "exponents": [ + 3, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0009238075886853635, + "original_integral": 0.0009238075886853645, + "original_abs_error": 9.75781955236954e-19, + "original_rel_error": 1.0562610300977862e-15, + "reduced_integral": 0.0009238075886853639, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 4.694493467101272e-16 + } + }, + { + "exponents": [ + 3, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0005369129622791243, + "original_integral": 0.0005369129622791253, + "original_abs_error": 9.75781955236954e-19, + "original_rel_error": 1.8173931787656776e-15, + "reduced_integral": 0.0005369129622791254, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 2.0193257541840863e-15 + } + }, + { + "exponents": [ + 3, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.003454498702450346, + "original_integral": 0.00345449870245035, + "original_abs_error": 3.903127820947816e-18, + "original_rel_error": 1.1298680813454201e-15, + "reduced_integral": 0.0034544987024503434, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 7.532453875636135e-16 + } + }, + { + "exponents": [ + 3, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0016304744645632033, + "original_integral": 0.0016304744645632041, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 5.319689187654747e-16, + "reduced_integral": 0.0016304744645632035, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 1.3299222969136868e-16 + } + }, + { + "exponents": [ + 3, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0008415295792815345, + "original_integral": 0.0008415295792815349, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 5.1534833673281195e-16, + "reduced_integral": 0.000841529579281535, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 6.441854209160149e-16 + } + }, + { + "exponents": [ + 3, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.00046512647960921324, + "original_integral": 0.0004651264796092134, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 3.4964753245066437e-16, + "reduced_integral": 0.00046512647960921373, + "reduced_abs_error": 4.87890977618477e-19, + "reduced_rel_error": 1.048942597351993e-15 + } + }, + { + "exponents": [ + 3, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0018680439957064418, + "original_integral": 0.001868043995706441, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 4.643154764994664e-16, + "reduced_integral": 0.001868043995706439, + "reduced_abs_error": 2.8189256484623115e-18, + "reduced_rel_error": 1.5090252986232656e-15 + } + }, + { + "exponents": [ + 3, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0008816903105274064, + "original_integral": 0.000881690310527407, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 7.378115600501225e-16, + "reduced_integral": 0.0008816903105274055, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 9.837487467334967e-16 + } + }, + { + "exponents": [ + 3, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.00045506292321695677, + "original_integral": 0.00045506292321695693, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 3.5737986457598014e-16, + "reduced_integral": 0.0004550629232169568, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 1.1912662152532672e-16 + } + }, + { + "exponents": [ + 3, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0010636696952878546, + "original_integral": 0.0010636696952878544, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 2.0386068669411387e-16, + "reduced_integral": 0.0010636696952878533, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 1.2231641201646832e-15 + } + }, + { + "exponents": [ + 3, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0005020370323677951, + "original_integral": 0.0005020370323677958, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 1.511724179310837e-15, + "reduced_integral": 0.0005020370323677949, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 4.319211940888106e-16 + } + }, + { + "exponents": [ + 3, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0006285380457970661, + "original_integral": 0.0006285380457970658, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 5.174876110055988e-16, + "reduced_integral": 0.0006285380457970649, + "reduced_abs_error": 1.1926223897340549e-18, + "reduced_rel_error": 1.897454573687195e-15 + } + }, + { + "exponents": [ + 4, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.008440467836218837, + "original_integral": 0.008440467836218851, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 1.6441965157741104e-15, + "reduced_integral": 0.00844046783621883, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 8.220982578870552e-16 + } + }, + { + "exponents": [ + 4, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.003983781283854647, + "original_integral": 0.003983781283854649, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 4.3544646464584893e-16, + "reduced_integral": 0.003983781283854646, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 2.1772323232292447e-16 + } + }, + { + "exponents": [ + 4, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0020561314271487006, + "original_integral": 0.0020561314271487037, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 1.4764455437409468e-15, + "reduced_integral": 0.0020561314271487006, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 4, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0011364558012803866, + "original_integral": 0.0011364558012803869, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.9080410716615452e-16, + "reduced_integral": 0.0011364558012803873, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 5.724123214984635e-16 + } + }, + { + "exponents": [ + 4, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0006605031807901359, + "original_integral": 0.0006605031807901366, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 9.848874652096417e-16, + "reduced_integral": 0.0006605031807901369, + "reduced_abs_error": 9.75781955236954e-19, + "reduced_rel_error": 1.4773311978144625e-15 + } + }, + { + "exponents": [ + 4, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.003865742269027963, + "original_integral": 0.0038657422690279637, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 2.243713309440313e-16, + "reduced_integral": 0.0038657422690279585, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 1.1218566547201566e-15 + } + }, + { + "exponents": [ + 4, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.001824575603910886, + "original_integral": 0.0018245756039108856, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.3768862636584016e-16, + "reduced_integral": 0.0018245756039108848, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 7.130658790975205e-16 + } + }, + { + "exponents": [ + 4, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0009417101424755005, + "original_integral": 0.0009417101424755011, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 6.90787190399435e-16, + "reduced_integral": 0.0009417101424755008, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 3.453935951997175e-16 + } + }, + { + "exponents": [ + 4, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.00052049783414135, + "original_integral": 0.0005204978341413503, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 4.1660199192724053e-16, + "reduced_integral": 0.0005204978341413508, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 1.4581069717453418e-15 + } + }, + { + "exponents": [ + 4, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.001946356703161814, + "original_integral": 0.0019463567031618122, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 8.91266987782243e-16, + "reduced_integral": 0.001946356703161812, + "reduced_abs_error": 1.951563910473908e-18, + "reduced_rel_error": 1.0026753612550233e-15 + } + }, + { + "exponents": [ + 4, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0009186527993731023, + "original_integral": 0.0009186527993731027, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 4.720835437394301e-16, + "reduced_integral": 0.0009186527993731023, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 4, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0004741402092239166, + "original_integral": 0.0004741402092239167, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 2.2866699583655876e-16, + "reduced_integral": 0.00047414020922391685, + "reduced_abs_error": 2.710505431213761e-19, + "reduced_rel_error": 5.71667489591397e-16 + } + }, + { + "exponents": [ + 4, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0010525058093857175, + "original_integral": 0.0010525058093857169, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 6.180690858808387e-16, + "reduced_integral": 0.0010525058093857156, + "reduced_abs_error": 1.951563910473908e-18, + "reduced_rel_error": 1.854207257642516e-15 + } + }, + { + "exponents": [ + 4, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0004967678363261753, + "original_integral": 0.000496767836326175, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 4.365025644589529e-16, + "reduced_integral": 0.0004967678363261756, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 6.547538466884294e-16 + } + }, + { + "exponents": [ + 4, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0005992998752337373, + "original_integral": 0.0005992998752337378, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 7.23645852295662e-16, + "reduced_integral": 0.0005992998752337364, + "reduced_abs_error": 9.75781955236954e-19, + "reduced_rel_error": 1.6282031676652395e-15 + } + }, + { + "exponents": [ + 5, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0049366547789406945, + "original_integral": 0.004936654778940692, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 5.270948305045477e-16, + "reduced_integral": 0.004936654778940695, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 1.7569827683484924e-16 + } + }, + { + "exponents": [ + 5, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.00233003114220808, + "original_integral": 0.002330031142208081, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 3.7225328120139994e-16, + "reduced_integral": 0.00233003114220808, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 5, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0012025886755242907, + "original_integral": 0.0012025886755242922, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 1.2621797231027118e-15, + "reduced_integral": 0.001202588675524291, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 1.8031138901467313e-16 + } + }, + { + "exponents": [ + 5, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0006646894545787399, + "original_integral": 0.0006646894545787413, + "original_abs_error": 1.4094628242311558e-18, + "original_rel_error": 2.120483203881173e-15, + "reduced_integral": 0.0006646894545787406, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 1.1417986482437085e-15 + } + }, + { + "exponents": [ + 5, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0022609925678123447, + "original_integral": 0.0022609925678123474, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 1.1508596936622816e-15, + "reduced_integral": 0.0022609925678123434, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 5.754298468311408e-16 + } + }, + { + "exponents": [ + 5, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0010671564715801782, + "original_integral": 0.0010671564715801784, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 2.0319460198373458e-16, + "reduced_integral": 0.0010671564715801782, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 5, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0005507867532270834, + "original_integral": 0.0005507867532270835, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 1.968460871894825e-16, + "reduced_integral": 0.0005507867532270837, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 5.905382615684475e-16 + } + }, + { + "exponents": [ + 5, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0011383837136320907, + "original_integral": 0.001138383713632092, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 1.1428858225945102e-15, + "reduced_integral": 0.0011383837136320894, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 1.1428858225945102e-15 + } + }, + { + "exponents": [ + 5, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0005373009909180692, + "original_integral": 0.0005373009909180693, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 2.0178674352209226e-16, + "reduced_integral": 0.0005373009909180686, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 1.0089337176104614e-15 + } + }, + { + "exponents": [ + 5, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0006155888434845912, + "original_integral": 0.000615588843484592, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 1.2328708175473134e-15, + "reduced_integral": 0.00061558884348459, + "reduced_abs_error": 1.1926223897340549e-18, + "reduced_rel_error": 1.9373684275743494e-15 + } + }, + { + "exponents": [ + 6, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0029669160778858826, + "original_integral": 0.00296691607788588, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 8.770336422253516e-16, + "reduced_integral": 0.00296691607788588, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 8.770336422253516e-16 + } + }, + { + "exponents": [ + 6, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0014003423709679262, + "original_integral": 0.0014003423709679273, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 7.742407820853813e-16, + "reduced_integral": 0.0014003423709679271, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 6.193926256683051e-16 + } + }, + { + "exponents": [ + 6, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0007227525189156776, + "original_integral": 0.0007227525189156776, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0007227525189156789, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 1.80012185766508e-15 + } + }, + { + "exponents": [ + 6, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0013588503757725524, + "original_integral": 0.0013588503757725515, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 6.383055511135867e-16, + "reduced_integral": 0.0013588503757725513, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 7.978819388919835e-16 + } + }, + { + "exponents": [ + 6, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0006413581331750713, + "original_integral": 0.0006413581331750718, + "original_abs_error": 5.421010862427522e-19, + "original_rel_error": 8.452392792760844e-16, + "reduced_integral": 0.0006413581331750716, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 5.071435675656506e-16 + } + }, + { + "exponents": [ + 6, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0006841655116712915, + "original_integral": 0.0006841655116712926, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 1.5847074340783363e-15, + "reduced_integral": 0.0006841655116712909, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 9.508244604470018e-16 + } + }, + { + "exponents": [ + 7, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0018197428512164628, + "original_integral": 0.001819742851216463, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.1915993204872175e-16, + "reduced_integral": 0.0018197428512164626, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 1.1915993204872175e-16 + } + }, + { + "exponents": [ + 7, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0008588928543742958, + "original_integral": 0.0008588928543742973, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 1.7672554076439317e-15, + "reduced_integral": 0.0008588928543742966, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 8.8362770382196585e-16 + } + }, + { + "exponents": [ + 7, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0008334439506448411, + "original_integral": 0.0008334439506448407, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 3.9026097855050124e-16, + "reduced_integral": 0.0008334439506448404, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 7.805219571010025e-16 + } + }, + { + "exponents": [ + 8, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 8, + "original_points": 486, + "reduced_points": 165, + "exact_integral": 0.0011337546528181915, + "original_integral": 0.001133754652818191, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 3.8251738849864434e-16, + "reduced_integral": 0.0011337546528181939, + "reduced_abs_error": 2.3852447794681098e-18, + "reduced_rel_error": 2.1038456367425437e-15 + } + } + ] + }, + { + "strength": 9, + "results": [ + { + "exponents": [ + 0, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.12499999999999997, + "original_integral": 0.125, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 2.2204460492503136e-16, + "reduced_integral": 0.12499999999999988, + "reduced_abs_error": 9.71445146547012e-17, + "reduced_rel_error": 7.771561172376098e-16 + } + }, + { + "exponents": [ + 0, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.05899822973615082, + "original_integral": 0.058998229736150876, + "original_abs_error": 5.551115123125783e-17, + "original_rel_error": 9.408952010850538e-16, + "reduced_integral": 0.058998229736150785, + "reduced_abs_error": 3.469446951953614e-17, + "reduced_rel_error": 5.880595006781586e-16 + } + }, + { + "exponents": [ + 0, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.030450495562663715, + "original_integral": 0.030450495562663757, + "original_abs_error": 4.163336342344337e-17, + "original_rel_error": 1.367247483305044e-15, + "reduced_integral": 0.03045049556266369, + "reduced_abs_error": 2.42861286636753e-17, + "reduced_rel_error": 7.975610319279422e-16 + } + }, + { + "exponents": [ + 0, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.016830462234625018, + "original_integral": 0.016830462234625046, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 1.649127351863685e-15, + "reduced_integral": 0.016830462234625, + "reduced_abs_error": 1.734723475976807e-17, + "reduced_rel_error": 1.0307045949148031e-15 + } + }, + { + "exponents": [ + 0, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00978179162587195, + "original_integral": 0.00978179162587195, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.009781791625871948, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 1.7734210074447112e-16 + } + }, + { + "exponents": [ + 0, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0058964926575299035, + "original_integral": 0.005896492657529905, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.941958171967773e-16, + "reduced_integral": 0.005896492657529901, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 4.4129372579516596e-16 + } + }, + { + "exponents": [ + 0, + 0, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0036511518202430055, + "original_integral": 0.0036511518202430085, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 8.314543553429571e-16, + "reduced_integral": 0.003651151820243009, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 9.502335489633796e-16 + } + }, + { + "exponents": [ + 0, + 0, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.002306984964566927, + "original_integral": 0.0023069849645669313, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 1.879859971586826e-15, + "reduced_integral": 0.00230698496456693, + "reduced_abs_error": 3.0357660829594124e-18, + "reduced_rel_error": 1.3159019801107781e-15 + } + }, + { + "exponents": [ + 0, + 0, + 8 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.001480624539659955, + "original_integral": 0.001480624539659957, + "original_abs_error": 1.951563910473908e-18, + "original_rel_error": 1.3180680572281413e-15, + "reduced_integral": 0.0014806245396599576, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 1.7574240763041884e-15 + } + }, + { + "exponents": [ + 0, + 0, + 9 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.000962107316018366, + "original_integral": 0.0009621073160183683, + "original_abs_error": 2.2768245622195593e-18, + "original_rel_error": 2.3664975042930617e-15, + "reduced_integral": 0.0009621073160183688, + "reduced_abs_error": 2.8189256484623115e-18, + "reduced_rel_error": 2.9299492910295052e-15 + } + }, + { + "exponents": [ + 0, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.057250118477433484, + "original_integral": 0.05725011847743346, + "original_abs_error": 2.0816681711721685e-17, + "original_rel_error": 3.6360940842292024e-16, + "reduced_integral": 0.057250118477433394, + "reduced_abs_error": 9.020562075079397e-17, + "reduced_rel_error": 1.5756407698326544e-15 + } + }, + { + "exponents": [ + 0, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.027021245138827792, + "original_integral": 0.02702124513882782, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 1.027176041408467e-15, + "reduced_integral": 0.027021245138827775, + "reduced_abs_error": 1.734723475976807e-17, + "reduced_rel_error": 6.419850258802919e-16 + } + }, + { + "exponents": [ + 0, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.013946355829272483, + "original_integral": 0.01394635582927249, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 4.975417226443446e-16, + "reduced_integral": 0.01394635582927249, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 4.975417226443446e-16 + } + }, + { + "exponents": [ + 0, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0077083676556980165, + "original_integral": 0.0077083676556980165, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.007708367655698018, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.2504420565546086e-16 + } + }, + { + "exponents": [ + 0, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.004480069836021888, + "original_integral": 0.004480069836021894, + "original_abs_error": 6.071532165918825e-18, + "original_rel_error": 1.3552315897178265e-15, + "reduced_integral": 0.004480069836021888, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 0, + 1, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.002700599225959228, + "original_integral": 0.002700599225959227, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 3.211738082611368e-16, + "reduced_integral": 0.0027005992259592306, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 9.635214247834104e-16 + } + }, + { + "exponents": [ + 0, + 1, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0016722309943040716, + "original_integral": 0.001672230994304074, + "original_abs_error": 2.3852447794681098e-18, + "original_rel_error": 1.4263847444478037e-15, + "reduced_integral": 0.001672230994304074, + "reduced_abs_error": 2.3852447794681098e-18, + "reduced_rel_error": 1.4263847444478037e-15 + } + }, + { + "exponents": [ + 0, + 1, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.001056601300376914, + "original_integral": 0.001056601300376916, + "original_abs_error": 1.951563910473908e-18, + "original_rel_error": 1.8470201671886454e-15, + "reduced_integral": 0.0010566013003769162, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 2.0522446302096062e-15 + } + }, + { + "exponents": [ + 0, + 1, + 8 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0006781274425290227, + "original_integral": 0.0006781274425290236, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.2790541771228819e-15, + "reduced_integral": 0.0006781274425290246, + "reduced_abs_error": 1.951563910473908e-18, + "reduced_rel_error": 2.8778718985264844e-15 + } + }, + { + "exponents": [ + 0, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.02882477519210804, + "original_integral": 0.028824775192108076, + "original_abs_error": 3.469446951953614e-17, + "original_rel_error": 1.2036336550175477e-15, + "reduced_integral": 0.02882477519210799, + "reduced_abs_error": 5.204170427930421e-17, + "reduced_rel_error": 1.8054504825263216e-15 + } + }, + { + "exponents": [ + 0, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.013604885671015126, + "original_integral": 0.013604885671015147, + "original_abs_error": 2.0816681711721685e-17, + "original_rel_error": 1.5300886913052942e-15, + "reduced_integral": 0.013604885671015119, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 5.100295637684314e-16 + } + }, + { + "exponents": [ + 0, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.007021829512656519, + "original_integral": 0.007021829512656529, + "original_abs_error": 9.540979117872439e-18, + "original_rel_error": 1.3587597221885365e-15, + "reduced_integral": 0.0070218295126565165, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 3.7057083332414633e-16 + } + }, + { + "exponents": [ + 0, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.003881074322338644, + "original_integral": 0.003881074322338643, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 2.234849595628851e-16, + "reduced_integral": 0.0038810743223386445, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 1.1174247978144255e-16 + } + }, + { + "exponents": [ + 0, + 2, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0022556635567344318, + "original_integral": 0.002255663556734433, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 5.767893013558059e-16, + "reduced_integral": 0.0022556635567344335, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 7.690524018077413e-16 + } + }, + { + "exponents": [ + 0, + 2, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0013597206022017202, + "original_integral": 0.001359720602201722, + "original_abs_error": 1.951563910473908e-18, + "original_rel_error": 1.4352683244733137e-15, + "reduced_integral": 0.0013597206022017225, + "reduced_abs_error": 2.3852447794681098e-18, + "reduced_rel_error": 1.754216841022939e-15 + } + }, + { + "exponents": [ + 0, + 2, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0008419490432860857, + "original_integral": 0.000841949043286086, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 3.8631869035229855e-16, + "reduced_integral": 0.0008419490432860865, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 9.014102774886965e-16 + } + }, + { + "exponents": [ + 0, + 2, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00053198658380172, + "original_integral": 0.0005319865838017204, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 8.152101616830278e-16, + "reduced_integral": 0.000531986583801721, + "reduced_abs_error": 9.75781955236954e-19, + "reduced_rel_error": 1.8342228637868127e-15 + } + }, + { + "exponents": [ + 0, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.015587195961894966, + "original_integral": 0.015587195961894949, + "original_abs_error": 1.734723475976807e-17, + "original_rel_error": 1.1129156778535254e-15, + "reduced_integral": 0.015587195961894935, + "reduced_abs_error": 3.122502256758253e-17, + "reduced_rel_error": 2.003248220136346e-15 + } + }, + { + "exponents": [ + 0, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.007356935746418251, + "original_integral": 0.0073569357464182555, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 5.894857369188534e-16, + "reduced_integral": 0.007356935746418243, + "reduced_abs_error": 7.806255641895632e-18, + "reduced_rel_error": 1.0610743264539362e-15 + } + }, + { + "exponents": [ + 0, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0037971027317764182, + "original_integral": 0.003797102731776424, + "original_abs_error": 5.637851296924623e-18, + "original_rel_error": 1.4847771301375978e-15, + "reduced_integral": 0.0037971027317764174, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 2.284272507903997e-16 + } + }, + { + "exponents": [ + 0, + 3, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.002098717703842982, + "original_integral": 0.0020987177038429843, + "original_abs_error": 2.168404344971009e-18, + "original_rel_error": 1.0332043899951017e-15, + "reduced_integral": 0.002098717703842982, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 0, + 3, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.001219765623447114, + "original_integral": 0.001219765623447115, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 7.110888529037235e-16, + "reduced_integral": 0.0012197656234471154, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 1.0666332793555854e-15 + } + }, + { + "exponents": [ + 0, + 3, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0007352782923263471, + "original_integral": 0.0007352782923263479, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.1796373523338458e-15, + "reduced_integral": 0.000735278292326348, + "reduced_abs_error": 9.75781955236954e-19, + "reduced_rel_error": 1.3270920213755765e-15 + } + }, + { + "exponents": [ + 0, + 3, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0004552897512700577, + "original_integral": 0.00045528975127005815, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 9.525381754902747e-16, + "reduced_integral": 0.00045528975127005826, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 1.1906727193628433e-15 + } + }, + { + "exponents": [ + 0, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.008875394807235764, + "original_integral": 0.008875394807235766, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.9545310531567051e-16, + "reduced_integral": 0.008875394807235749, + "reduced_abs_error": 1.5612511283791264e-17, + "reduced_rel_error": 1.7590779478410346e-15 + } + }, + { + "exponents": [ + 0, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.004189060654690686, + "original_integral": 0.004189060654690683, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 6.211619808014799e-16, + "reduced_integral": 0.004189060654690679, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 1.6564319488039465e-15 + } + }, + { + "exponents": [ + 0, + 4, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00216208136155697, + "original_integral": 0.0021620813615569707, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 4.01169795647188e-16, + "reduced_integral": 0.0021620813615569686, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 6.01754693470782e-16 + } + }, + { + "exponents": [ + 0, + 4, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0011950159769645481, + "original_integral": 0.0011950159769645499, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.4516320362370101e-15, + "reduced_integral": 0.001195015976964548, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 1.8145400452962627e-16 + } + }, + { + "exponents": [ + 0, + 4, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0006945381008138094, + "original_integral": 0.0006945381008138092, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 3.1220811967410134e-16, + "reduced_integral": 0.0006945381008138102, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 1.0927284188593549e-15 + } + }, + { + "exponents": [ + 0, + 4, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00041866960250835743, + "original_integral": 0.00041866960250835797, + "original_abs_error": 5.421010862427522e-19, + "original_rel_error": 1.2948183555598137e-15, + "reduced_integral": 0.00041866960250835786, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 1.035854684447851e-15 + } + }, + { + "exponents": [ + 0, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.005244601150649224, + "original_integral": 0.005244601150649229, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 9.922909823726447e-16, + "reduced_integral": 0.00524460115064921, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 2.6461092863270526e-15 + } + }, + { + "exponents": [ + 0, + 5, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.002475377468483872, + "original_integral": 0.0024753774684838697, + "original_abs_error": 2.168404344971009e-18, + "original_rel_error": 8.759893683201055e-16, + "reduced_integral": 0.0024753774684838654, + "reduced_abs_error": 6.5052130349130266e-18, + "reduced_rel_error": 2.6279681049603167e-15 + } + }, + { + "exponents": [ + 0, + 5, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.001277605632526282, + "original_integral": 0.001277605632526281, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 8.486203761810678e-16, + "reduced_integral": 0.0012776056325262825, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 3.394481504724271e-16 + } + }, + { + "exponents": [ + 0, + 5, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0007061524928133816, + "original_integral": 0.0007061524928133826, + "original_abs_error": 9.75781955236954e-19, + "original_rel_error": 1.3818289465343978e-15, + "reduced_integral": 0.0007061524928133814, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 3.0707309922986617e-16 + } + }, + { + "exponents": [ + 0, + 5, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00041041276493167196, + "original_integral": 0.00041041276493167256, + "original_abs_error": 5.963111948670274e-19, + "original_rel_error": 1.4529547953175993e-15, + "reduced_integral": 0.00041041276493167223, + "reduced_abs_error": 2.710505431213761e-19, + "reduced_rel_error": 6.604339978716361e-16 + } + }, + { + "exponents": [ + 0, + 6, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0031841740489256596, + "original_integral": 0.0031841740489256583, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 4.0859657386554515e-16, + "reduced_integral": 0.003184174048925651, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 2.7239771591036346e-15 + } + }, + { + "exponents": [ + 0, + 6, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0015028850564672448, + "original_integral": 0.0015028850564672465, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.1542622428188441e-15, + "reduced_integral": 0.0015028850564672422, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 1.7313933642282662e-15 + } + }, + { + "exponents": [ + 0, + 6, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0007756774219804779, + "original_integral": 0.0007756774219804781, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 2.795497565772364e-16, + "reduced_integral": 0.0007756774219804776, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 4.1932463486585463e-16 + } + }, + { + "exponents": [ + 0, + 6, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0004287289686313307, + "original_integral": 0.00042872896863133087, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 3.7933132065231046e-16, + "reduced_integral": 0.00042872896863133065, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 1.2644377355077015e-16 + } + }, + { + "exponents": [ + 0, + 7, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.001972861658829849, + "original_integral": 0.0019728616588298494, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.198232537254681e-16, + "reduced_integral": 0.0019728616588298438, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 2.637879044705617e-15 + } + }, + { + "exponents": [ + 0, + 7, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0009311627630822963, + "original_integral": 0.0009311627630822967, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 4.657412067882197e-16, + "reduced_integral": 0.0009311627630822945, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 1.862964827152879e-15 + } + }, + { + "exponents": [ + 0, + 7, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0004805969215035816, + "original_integral": 0.00048059692150358233, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 1.5791643408065344e-15, + "reduced_integral": 0.00048059692150358147, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 2.255949058295049e-16 + } + }, + { + "exponents": [ + 0, + 8, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0012416379281119064, + "original_integral": 0.001241637928111907, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 5.239219008721135e-16, + "reduced_integral": 0.001241637928111902, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 3.492812672480757e-15 + } + }, + { + "exponents": [ + 0, + 8, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0005860355178549166, + "original_integral": 0.0005860355178549174, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.4800497778073825e-15, + "reduced_integral": 0.0005860355178549154, + "reduced_abs_error": 1.1926223897340549e-18, + "reduced_rel_error": 2.0350684444851508e-15 + } + }, + { + "exponents": [ + 0, + 9, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0007911818565258345, + "original_integral": 0.0007911818565258339, + "original_abs_error": 5.421010862427522e-19, + "original_rel_error": 6.851788647216671e-16, + "reduced_integral": 0.0007911818565258308, + "reduced_abs_error": 3.686287386450715e-18, + "reduced_rel_error": 4.659216280107337e-15 + } + }, + { + "exponents": [ + 1, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.056360767612003086, + "original_integral": 0.05636076761200307, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 2.4623134843285776e-16, + "reduced_integral": 0.056360767612002996, + "reduced_abs_error": 9.020562075079397e-17, + "reduced_rel_error": 1.6005037648135754e-15 + } + }, + { + "exponents": [ + 1, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.02660148412543013, + "original_integral": 0.026601484125430137, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 2.6084611938150766e-16, + "reduced_integral": 0.026601484125430105, + "reduced_abs_error": 2.42861286636753e-17, + "reduced_rel_error": 9.129614178352768e-16 + } + }, + { + "exponents": [ + 1, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.013729706432620965, + "original_integral": 0.013729706432620954, + "original_abs_error": 1.0408340855860843e-17, + "original_rel_error": 7.580891045952188e-16, + "reduced_integral": 0.013729706432620949, + "reduced_abs_error": 1.5612511283791264e-17, + "reduced_rel_error": 1.137133656892828e-15 + } + }, + { + "exponents": [ + 1, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.007588622166466357, + "original_integral": 0.007588622166466358, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.1429765759339295e-16, + "reduced_integral": 0.007588622166466348, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 1.1429765759339296e-15 + } + }, + { + "exponents": [ + 1, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.004410474277238454, + "original_integral": 0.004410474277238458, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 9.832975814695012e-16, + "reduced_integral": 0.004410474277238454, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 1, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0026586468191754017, + "original_integral": 0.0026586468191754043, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 9.78725415951362e-16, + "reduced_integral": 0.0026586468191754004, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 4.89362707975681e-16 + } + }, + { + "exponents": [ + 1, + 0, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0016462537540548646, + "original_integral": 0.0016462537540548675, + "original_abs_error": 2.8189256484623115e-18, + "original_rel_error": 1.7123275445957557e-15, + "reduced_integral": 0.001646253754054864, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 3.9515251029132827e-16 + } + }, + { + "exponents": [ + 1, + 0, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.001040187547778734, + "original_integral": 0.0010401875477787355, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 1.459239773366895e-15, + "reduced_integral": 0.0010401875477787346, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 6.253884743000979e-16 + } + }, + { + "exponents": [ + 1, + 0, + 8 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0006675930848032302, + "original_integral": 0.0006675930848032313, + "original_abs_error": 1.1926223897340549e-18, + "original_rel_error": 1.786451083575221e-15, + "reduced_integral": 0.0006675930848032308, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 9.744278637683023e-16 + } + }, + { + "exponents": [ + 1, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.02581328498613018, + "original_integral": 0.025813284986130173, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 2.6881095945888283e-16, + "reduced_integral": 0.02581328498613014, + "reduced_abs_error": 3.8163916471489756e-17, + "reduced_rel_error": 1.4784602770238555e-15 + } + }, + { + "exponents": [ + 1, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.01218350494285153, + "original_integral": 0.012183504942851536, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 4.271488748386713e-16, + "reduced_integral": 0.012183504942851517, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 1.1390636662364568e-15 + } + }, + { + "exponents": [ + 1, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.006288218559423448, + "original_integral": 0.006288218559423449, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.758688266929227e-16, + "reduced_integral": 0.006288218559423446, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.758688266929227e-16 + } + }, + { + "exponents": [ + 1, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0034755961448854167, + "original_integral": 0.003475596144885417, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.2477884394951743e-16, + "reduced_integral": 0.0034755961448854136, + "reduced_abs_error": 3.0357660829594124e-18, + "reduced_rel_error": 8.73451907646622e-16 + } + }, + { + "exponents": [ + 1, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.002020001399308595, + "original_integral": 0.002020001399308598, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 1.502853455447354e-15, + "reduced_integral": 0.0020200013993085944, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 2.1469335077819344e-16 + } + }, + { + "exponents": [ + 1, + 1, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0012176627630995475, + "original_integral": 0.0012176627630995486, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 8.903960976236797e-16, + "reduced_integral": 0.0012176627630995473, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 1.7807921952473598e-16 + } + }, + { + "exponents": [ + 1, + 1, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0007539857797084852, + "original_integral": 0.0007539857797084867, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 2.013145449595307e-15, + "reduced_integral": 0.0007539857797084855, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 4.313883106275657e-16 + } + }, + { + "exponents": [ + 1, + 1, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0004764068827926682, + "original_integral": 0.0004764068827926688, + "original_abs_error": 5.963111948670274e-19, + "original_rel_error": 1.2516846762823565e-15, + "reduced_integral": 0.0004764068827926685, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 6.827370961540126e-16 + } + }, + { + "exponents": [ + 1, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.012996691648565066, + "original_integral": 0.012996691648565059, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 5.338969402011884e-16, + "reduced_integral": 0.01299669164856504, + "reduced_abs_error": 2.6020852139652106e-17, + "reduced_rel_error": 2.0021135257544566e-15 + } + }, + { + "exponents": [ + 1, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.006134254397535635, + "original_integral": 0.006134254397535638, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 4.241893220161472e-16, + "reduced_integral": 0.0061342543975356255, + "reduced_abs_error": 9.540979117872439e-18, + "reduced_rel_error": 1.5553608473925397e-15 + } + }, + { + "exponents": [ + 1, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.003166045610991512, + "original_integral": 0.0031660456109915158, + "original_abs_error": 3.903127820947816e-18, + "original_rel_error": 1.2328084622019933e-15, + "reduced_integral": 0.0031660456109915097, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 6.848935901122184e-16 + } + }, + { + "exponents": [ + 1, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.001749922623729925, + "original_integral": 0.0017499226237299257, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 3.717428957542874e-16, + "reduced_integral": 0.0017499226237299229, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 1.2391429858476248e-15 + } + }, + { + "exponents": [ + 1, + 2, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0010170474362557894, + "original_integral": 0.001017047436255791, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 1.4924407528794515e-15, + "reduced_integral": 0.001017047436255789, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 4.2641164367984326e-16 + } + }, + { + "exponents": [ + 1, + 2, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0006130791750235527, + "original_integral": 0.0006130791750235534, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 1.0610722562323399e-15, + "reduced_integral": 0.0006130791750235522, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 8.842268801936166e-16 + } + }, + { + "exponents": [ + 1, + 2, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0003796231549583632, + "original_integral": 0.0003796231549583636, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.142398358292364e-15, + "reduced_integral": 0.00037962315495836325, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 1.427997947865455e-16 + } + }, + { + "exponents": [ + 1, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.007028050634648921, + "original_integral": 0.007028050634648921, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.007028050634648905, + "reduced_abs_error": 1.6479873021779667e-17, + "reduced_rel_error": 2.3448711283513544e-15 + } + }, + { + "exponents": [ + 1, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.003317140367522541, + "original_integral": 0.0033171403675225425, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 3.9221813454771275e-16, + "reduced_integral": 0.003317140367522537, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 1.3073937818257092e-15 + } + }, + { + "exponents": [ + 1, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0017120609973164226, + "original_integral": 0.0017120609973164224, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.2665461968760946e-16, + "reduced_integral": 0.001712060997316421, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 1.0132369575008757e-15 + } + }, + { + "exponents": [ + 1, + 3, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.000946282726315928, + "original_integral": 0.0009462827263159287, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 8.020240670508353e-16, + "reduced_integral": 0.0009462827263159273, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 6.874492003292874e-16 + } + }, + { + "exponents": [ + 1, + 3, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0005499754147537028, + "original_integral": 0.0005499754147537029, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 1.9713647981356513e-16, + "reduced_integral": 0.0005499754147537026, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 3.9427295962713025e-16 + } + }, + { + "exponents": [ + 1, + 3, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00033152679171164574, + "original_integral": 0.00033152679171164607, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 9.810991445558808e-16, + "reduced_integral": 0.0003315267917116455, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 6.540660963705873e-16 + } + }, + { + "exponents": [ + 1, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.004001792513563149, + "original_integral": 0.004001792513563149, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.00400179251356314, + "reduced_abs_error": 9.540979117872439e-18, + "reduced_rel_error": 2.384176362351496e-15 + } + }, + { + "exponents": [ + 1, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0018887893925728578, + "original_integral": 0.0018887893925728573, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.29607848656675e-16, + "reduced_integral": 0.0018887893925728539, + "reduced_abs_error": 3.903127820947816e-18, + "reduced_rel_error": 2.066470637910075e-15 + } + }, + { + "exponents": [ + 1, + 4, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0009748525214156445, + "original_integral": 0.0009748525214156456, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 1.1121704551895363e-15, + "reduced_integral": 0.0009748525214156433, + "reduced_abs_error": 1.1926223897340549e-18, + "reduced_rel_error": 1.2233875007084898e-15 + } + }, + { + "exponents": [ + 1, + 4, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0005388161421626378, + "original_integral": 0.0005388161421626381, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 6.036579573138953e-16, + "reduced_integral": 0.0005388161421626371, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 1.2073159146277906e-15 + } + }, + { + "exponents": [ + 1, + 4, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0003131576039811927, + "original_integral": 0.0003131576039811929, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 6.924322824686182e-16, + "reduced_integral": 0.00031315760398119226, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 1.3848645649372364e-15 + } + }, + { + "exponents": [ + 1, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0023647179733550794, + "original_integral": 0.0023647179733550763, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 1.2837751127895581e-15, + "reduced_integral": 0.002364717973355074, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 2.200757336210671e-15 + } + }, + { + "exponents": [ + 1, + 5, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0011161133940256634, + "original_integral": 0.0011161133940256634, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0011161133940256617, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 1.5542537929053109e-15 + } + }, + { + "exponents": [ + 1, + 5, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0005760546732367999, + "original_integral": 0.0005760546732368001, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 3.7642335800991564e-16, + "reduced_integral": 0.0005760546732367988, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 1.8821167900495784e-15 + } + }, + { + "exponents": [ + 1, + 5, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00031839437236873334, + "original_integral": 0.0003183943723687332, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 5.107826644765036e-16, + "reduced_integral": 0.0003183943723687333, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 1.7026088815883453e-16 + } + }, + { + "exponents": [ + 1, + 6, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0014356999488613598, + "original_integral": 0.00143569994886136, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.5103464666769333e-16, + "reduced_integral": 0.0014356999488613557, + "reduced_abs_error": 4.119968255444917e-18, + "reduced_rel_error": 2.8696582866861737e-15 + } + }, + { + "exponents": [ + 1, + 6, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0006776300433208199, + "original_integral": 0.0006776300433208202, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 3.1999825957309135e-16, + "reduced_integral": 0.0006776300433208179, + "reduced_abs_error": 2.0599841277224584e-18, + "reduced_rel_error": 3.0399834659443678e-15 + } + }, + { + "exponents": [ + 1, + 6, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00034974219937695494, + "original_integral": 0.00034974219937695505, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 3.100003872615162e-16, + "reduced_integral": 0.0003497421993769544, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 1.5500019363075811e-15 + } + }, + { + "exponents": [ + 1, + 7, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0008895359798715205, + "original_integral": 0.0008895359798715203, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 2.4376803120253786e-16, + "reduced_integral": 0.0008895359798715173, + "reduced_abs_error": 3.2526065174565133e-18, + "reduced_rel_error": 3.656520468038068e-15 + } + }, + { + "exponents": [ + 1, + 7, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00041984838479225585, + "original_integral": 0.0004198483847922565, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 1.5494195691932589e-15, + "reduced_integral": 0.0004198483847922547, + "reduced_abs_error": 1.1384122811097797e-18, + "reduced_rel_error": 2.7114842460882033e-15 + } + }, + { + "exponents": [ + 1, + 8, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.000559837333796513, + "original_integral": 0.0005598373337965132, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 3.8732757071881353e-16, + "reduced_integral": 0.0005598373337965113, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 3.0986205657505082e-15 + } + }, + { + "exponents": [ + 2, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.02801645567318039, + "original_integral": 0.028016455673180386, + "original_abs_error": 3.469446951953614e-18, + "original_rel_error": 1.2383604094770806e-16, + "reduced_integral": 0.028016455673180344, + "reduced_abs_error": 4.5102810375396984e-17, + "reduced_rel_error": 1.6098685323202047e-15 + } + }, + { + "exponents": [ + 2, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.013223370305591857, + "original_integral": 0.013223370305591873, + "original_abs_error": 1.5612511283791264e-17, + "original_rel_error": 1.1806756464491579e-15, + "reduced_integral": 0.013223370305591831, + "reduced_abs_error": 2.6020852139652106e-17, + "reduced_rel_error": 1.96779274408193e-15 + } + }, + { + "exponents": [ + 2, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.006824919673261954, + "original_integral": 0.006824919673261962, + "original_abs_error": 8.673617379884035e-18, + "original_rel_error": 1.270874646900936e-15, + "reduced_integral": 0.006824919673261943, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 1.525049576281123e-15 + } + }, + { + "exponents": [ + 2, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0037722391932440667, + "original_integral": 0.003772239193244071, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 1.149664288974324e-15, + "reduced_integral": 0.0037722391932440646, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 5.74832144487162e-16 + } + }, + { + "exponents": [ + 2, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0021924090519242294, + "original_integral": 0.0021924090519242294, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0021924090519242273, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 9.890509907664574e-16 + } + }, + { + "exponents": [ + 2, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0013215906013353603, + "original_integral": 0.0013215906013353607, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 3.281506909598195e-16, + "reduced_integral": 0.00132159060133536, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 1.6407534547990974e-16 + } + }, + { + "exponents": [ + 2, + 0, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0008183386650231205, + "original_integral": 0.0008183386650231222, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.1198112103474863e-15, + "reduced_integral": 0.0008183386650231191, + "reduced_abs_error": 1.4094628242311558e-18, + "reduced_rel_error": 1.7223466084073325e-15 + } + }, + { + "exponents": [ + 2, + 0, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0005170683359878634, + "original_integral": 0.0005170683359878644, + "original_abs_error": 9.75781955236954e-19, + "original_rel_error": 1.887143124656269e-15, + "reduced_integral": 0.0005170683359878634, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 2, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.012831563252858731, + "original_integral": 0.01283156325285873, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.351919046644866e-16, + "reduced_integral": 0.012831563252858695, + "reduced_abs_error": 3.642919299551295e-17, + "reduced_rel_error": 2.8390299979542185e-15 + } + }, + { + "exponents": [ + 2, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00605631613332888, + "original_integral": 0.006056316133328876, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 7.160803026902547e-16, + "reduced_integral": 0.006056316133328871, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 1.4321606053805095e-15 + } + }, + { + "exponents": [ + 2, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0031258196791457077, + "original_integral": 0.003125819679145709, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 4.1622445967138537e-16, + "reduced_integral": 0.0031258196791457034, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 1.3874148655712845e-15 + } + }, + { + "exponents": [ + 2, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0017276891259075272, + "original_integral": 0.0017276891259075291, + "original_abs_error": 1.951563910473908e-18, + "original_rel_error": 1.1295804790394702e-15, + "reduced_integral": 0.0017276891259075256, + "reduced_abs_error": 1.5178830414797062e-18, + "reduced_rel_error": 8.785625948084767e-16 + } + }, + { + "exponents": [ + 2, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.001004125423789278, + "original_integral": 0.0010041254237892787, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 6.478486532453526e-16, + "reduced_integral": 0.0010041254237892771, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 8.637982043271367e-16 + } + }, + { + "exponents": [ + 2, + 1, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0006052897480408958, + "original_integral": 0.0006052897480408965, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 1.2538482985979403e-15, + "reduced_integral": 0.0006052897480408954, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 7.164847420559659e-16 + } + }, + { + "exponents": [ + 2, + 1, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00037479988421790716, + "original_integral": 0.0003747998842179076, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.1570997944654152e-15, + "reduced_integral": 0.0003747998842179071, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 1.446374743081769e-16 + } + }, + { + "exponents": [ + 2, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.006460544291672678, + "original_integral": 0.006460544291672669, + "original_abs_error": 8.673617379884035e-18, + "original_rel_error": 1.3425521114473126e-15, + "reduced_integral": 0.006460544291672663, + "reduced_abs_error": 1.474514954580286e-17, + "reduced_rel_error": 2.282338589460431e-15 + } + }, + { + "exponents": [ + 2, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0030492854107254594, + "original_integral": 0.003049285410725459, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.4222377068043107e-16, + "reduced_integral": 0.003049285410725455, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 1.4222377068043107e-15 + } + }, + { + "exponents": [ + 2, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0015738142022877699, + "original_integral": 0.0015738142022877703, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.7556039865683194e-16, + "reduced_integral": 0.0015738142022877675, + "reduced_abs_error": 2.3852447794681098e-18, + "reduced_rel_error": 1.5155821926125756e-15 + } + }, + { + "exponents": [ + 2, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0008698715737289538, + "original_integral": 0.0008698715737289542, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 4.9855735270794565e-16, + "reduced_integral": 0.0008698715737289535, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 3.739180145309592e-16 + } + }, + { + "exponents": [ + 2, + 2, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.000505565584406869, + "original_integral": 0.0005055655844068694, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 8.578132736289743e-16, + "reduced_integral": 0.0005055655844068685, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 8.578132736289743e-16 + } + }, + { + "exponents": [ + 2, + 2, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0003047564158359572, + "original_integral": 0.00030475641583595773, + "original_abs_error": 5.421010862427522e-19, + "original_rel_error": 1.7788012264015855e-15, + "reduced_integral": 0.000304756415835957, + "reduced_abs_error": 1.6263032587282567e-19, + "reduced_rel_error": 5.336403679204756e-16 + } + }, + { + "exponents": [ + 2, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0034935838778848544, + "original_integral": 0.003493583877884855, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.241363837689703e-16, + "reduced_integral": 0.0034935838778848453, + "reduced_abs_error": 9.107298248878237e-18, + "reduced_rel_error": 2.6068640591483765e-15 + } + }, + { + "exponents": [ + 2, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.001648922113839706, + "original_integral": 0.0016489221138397056, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.630087045071678e-16, + "reduced_integral": 0.0016489221138397037, + "reduced_abs_error": 2.3852447794681098e-18, + "reduced_rel_error": 1.446547874789423e-15 + } + }, + { + "exponents": [ + 2, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0008510508829706097, + "original_integral": 0.0008510508829706093, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 5.09582773100981e-16, + "reduced_integral": 0.0008510508829706085, + "reduced_abs_error": 1.1926223897340549e-18, + "reduced_rel_error": 1.401352626027698e-15 + } + }, + { + "exponents": [ + 2, + 3, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0004703890521618867, + "original_integral": 0.00047038905216188715, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 9.21962080114374e-16, + "reduced_integral": 0.000470389052161886, + "reduced_abs_error": 7.047314121155779e-19, + "reduced_rel_error": 1.4981883801858575e-15 + } + }, + { + "exponents": [ + 2, + 3, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00027338807616780254, + "original_integral": 0.0002733880761678029, + "original_abs_error": 3.7947076036992655e-19, + "original_rel_error": 1.3880296671644585e-15, + "reduced_integral": 0.00027338807616780227, + "reduced_abs_error": 2.710505431213761e-19, + "reduced_rel_error": 9.914497622603275e-16 + } + }, + { + "exponents": [ + 2, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.001989256841591169, + "original_integral": 0.0019892568415911713, + "original_abs_error": 2.168404344971009e-18, + "original_rel_error": 1.0900575026986173e-15, + "reduced_integral": 0.0019892568415911657, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 1.7440920043177877e-15 + } + }, + { + "exponents": [ + 2, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0009389010571552447, + "original_integral": 0.0009389010571552455, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 8.08329605080383e-16, + "reduced_integral": 0.0009389010571552427, + "reduced_abs_error": 2.0599841277224584e-18, + "reduced_rel_error": 2.194037499503897e-15 + } + }, + { + "exponents": [ + 2, + 4, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00048459085302296267, + "original_integral": 0.00048459085302296305, + "original_abs_error": 3.7947076036992655e-19, + "original_rel_error": 7.830745421683498e-16, + "reduced_integral": 0.0004845908530229622, + "reduced_abs_error": 4.87890977618477e-19, + "reduced_rel_error": 1.006810125645021e-15 + } + }, + { + "exponents": [ + 2, + 4, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.000267840897178957, + "original_integral": 0.00026784089717895703, + "original_abs_error": 5.421010862427522e-20, + "original_rel_error": 2.0239668099698354e-16, + "reduced_integral": 0.0002678408971789566, + "reduced_abs_error": 3.7947076036992655e-19, + "reduced_rel_error": 1.4167767669788847e-15 + } + }, + { + "exponents": [ + 2, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0011754810852853989, + "original_integral": 0.0011754810852853995, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 5.534085674661115e-16, + "reduced_integral": 0.0011754810852853956, + "reduced_abs_error": 3.2526065174565133e-18, + "reduced_rel_error": 2.7670428373305578e-15 + } + }, + { + "exponents": [ + 2, + 5, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.000554810424961343, + "original_integral": 0.0005548104249613433, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 5.862554795510813e-16, + "reduced_integral": 0.0005548104249613417, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 2.345021918204325e-15 + } + }, + { + "exponents": [ + 2, + 5, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00028635185257182527, + "original_integral": 0.0002863518525718254, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 3.78625862814544e-16, + "reduced_integral": 0.00028635185257182467, + "reduced_abs_error": 5.963111948670274e-19, + "reduced_rel_error": 2.082442245479992e-15 + } + }, + { + "exponents": [ + 2, + 6, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0007136741687793364, + "original_integral": 0.0007136741687793378, + "original_abs_error": 1.4094628242311558e-18, + "original_rel_error": 1.9749388248728292e-15, + "reduced_integral": 0.0007136741687793339, + "reduced_abs_error": 2.4936649967166602e-18, + "reduced_rel_error": 3.494122536313467e-15 + } + }, + { + "exponents": [ + 2, + 6, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00033684410053119816, + "original_integral": 0.0003368441005311984, + "original_abs_error": 2.710505431213761e-19, + "original_rel_error": 8.046765334287685e-16, + "reduced_integral": 0.00033684410053119707, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 3.218706133715074e-15 + } + }, + { + "exponents": [ + 2, + 7, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00044218072971138883, + "original_integral": 0.00044218072971138894, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 2.4519435145741485e-16, + "reduced_integral": 0.0004421807297113875, + "reduced_abs_error": 1.3552527156068805e-18, + "reduced_rel_error": 3.064929393217686e-15 + } + }, + { + "exponents": [ + 3, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.01498059689723164, + "original_integral": 0.014980596897231625, + "original_abs_error": 1.5612511283791264e-17, + "original_rel_error": 1.0421821901286456e-15, + "reduced_integral": 0.014980596897231612, + "reduced_abs_error": 2.7755575615628914e-17, + "reduced_rel_error": 1.8527683380064813e-15 + } + }, + { + "exponents": [ + 3, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.007070629578620323, + "original_integral": 0.007070629578620324, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.2267107594082873e-16, + "reduced_integral": 0.007070629578620307, + "reduced_abs_error": 1.5612511283791264e-17, + "reduced_rel_error": 2.2080793669349175e-15 + } + }, + { + "exponents": [ + 3, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0036493327947616466, + "original_integral": 0.0036493327947616453, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 3.5651519884680235e-16, + "reduced_integral": 0.0036493327947616414, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 1.4260607953872094e-15 + } + }, + { + "exponents": [ + 3, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.002017042962647982, + "original_integral": 0.0020170429626479823, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.150082457464683e-16, + "reduced_integral": 0.0020170429626479797, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 1.0750412287323415e-15 + } + }, + { + "exponents": [ + 3, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0011722966182392302, + "original_integral": 0.0011722966182392306, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 3.699412437490293e-16, + "reduced_integral": 0.0011722966182392276, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 2.2196474624941756e-15 + } + }, + { + "exponents": [ + 3, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0007066638368795324, + "original_integral": 0.0007066638368795335, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 1.534254501083706e-15, + "reduced_integral": 0.000706663836879531, + "reduced_abs_error": 1.4094628242311558e-18, + "reduced_rel_error": 1.994530851408818e-15 + } + }, + { + "exponents": [ + 3, + 0, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00043757146903723206, + "original_integral": 0.00043757146903723303, + "original_abs_error": 9.75781955236954e-19, + "original_rel_error": 2.229994467838411e-15, + "reduced_integral": 0.0004375714690372312, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 1.982217304745254e-15 + } + }, + { + "exponents": [ + 3, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.006861127577833471, + "original_integral": 0.006861127577833473, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.528335840279738e-16, + "reduced_integral": 0.006861127577833457, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 2.0226686722237902e-15 + } + }, + { + "exponents": [ + 3, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.003238355048688473, + "original_integral": 0.0032383550486884717, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 4.0176033431217643e-16, + "reduced_integral": 0.003238355048688468, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 1.6070413372487057e-15 + } + }, + { + "exponents": [ + 3, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0016713978789095016, + "original_integral": 0.0016713978789095034, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.0378878050920001e-15, + "reduced_integral": 0.0016713978789095003, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 7.78415853819e-16 + } + }, + { + "exponents": [ + 3, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0009238075886853635, + "original_integral": 0.0009238075886853645, + "original_abs_error": 9.75781955236954e-19, + "original_rel_error": 1.0562610300977862e-15, + "reduced_integral": 0.0009238075886853618, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 1.8777973868405087e-15 + } + }, + { + "exponents": [ + 3, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0005369129622791243, + "original_integral": 0.0005369129622791253, + "original_abs_error": 9.75781955236954e-19, + "original_rel_error": 1.8173931787656776e-15, + "reduced_integral": 0.0005369129622791237, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 1.0096628770920431e-15 + } + }, + { + "exponents": [ + 3, + 1, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0003236527070805676, + "original_integral": 0.0003236527070805681, + "original_abs_error": 4.87890977618477e-19, + "original_rel_error": 1.5074521761902803e-15, + "reduced_integral": 0.0003236527070805677, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 3.3498937248672895e-16 + } + }, + { + "exponents": [ + 3, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.003454498702450346, + "original_integral": 0.00345449870245035, + "original_abs_error": 3.903127820947816e-18, + "original_rel_error": 1.1298680813454201e-15, + "reduced_integral": 0.0034544987024503386, + "reduced_abs_error": 7.37257477290143e-18, + "reduced_rel_error": 2.1341952647635716e-15 + } + }, + { + "exponents": [ + 3, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0016304744645632033, + "original_integral": 0.0016304744645632041, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 5.319689187654747e-16, + "reduced_integral": 0.001630474464563199, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 2.6598445938273735e-15 + } + }, + { + "exponents": [ + 3, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0008415295792815345, + "original_integral": 0.0008415295792815349, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 5.1534833673281195e-16, + "reduced_integral": 0.0008415295792815336, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 1.0306966734656239e-15 + } + }, + { + "exponents": [ + 3, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00046512647960921324, + "original_integral": 0.0004651264796092134, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 3.4964753245066437e-16, + "reduced_integral": 0.0004651264796092126, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 1.3985901298026575e-15 + } + }, + { + "exponents": [ + 3, + 2, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00027032949183371447, + "original_integral": 0.00027032949183371485, + "original_abs_error": 3.7947076036992655e-19, + "original_rel_error": 1.4037342274269774e-15, + "reduced_integral": 0.00027032949183371414, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 1.2032007663659805e-15 + } + }, + { + "exponents": [ + 3, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0018680439957064418, + "original_integral": 0.001868043995706441, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 4.643154764994664e-16, + "reduced_integral": 0.0018680439957064379, + "reduced_abs_error": 3.903127820947816e-18, + "reduced_rel_error": 2.0894196442475987e-15 + } + }, + { + "exponents": [ + 3, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0008816903105274064, + "original_integral": 0.000881690310527407, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 7.378115600501225e-16, + "reduced_integral": 0.0008816903105274044, + "reduced_abs_error": 1.951563910473908e-18, + "reduced_rel_error": 2.2134346801503676e-15 + } + }, + { + "exponents": [ + 3, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00045506292321695677, + "original_integral": 0.00045506292321695693, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 3.5737986457598014e-16, + "reduced_integral": 0.0004550629232169562, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 1.1912662152532672e-15 + } + }, + { + "exponents": [ + 3, + 3, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00025152035137884234, + "original_integral": 0.00025152035137884256, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 8.621188436974381e-16, + "reduced_integral": 0.00025152035137884185, + "reduced_abs_error": 4.87890977618477e-19, + "reduced_rel_error": 1.9397673983192356e-15 + } + }, + { + "exponents": [ + 3, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0010636696952878546, + "original_integral": 0.0010636696952878544, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 2.0386068669411387e-16, + "reduced_integral": 0.001063669695287853, + "reduced_abs_error": 1.5178830414797062e-18, + "reduced_rel_error": 1.427024806858797e-15 + } + }, + { + "exponents": [ + 3, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0005020370323677951, + "original_integral": 0.0005020370323677958, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 1.511724179310837e-15, + "reduced_integral": 0.0005020370323677944, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 1.2957635822664316e-15 + } + }, + { + "exponents": [ + 3, + 4, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0002591141546920216, + "original_integral": 0.00025911415469202147, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 4.1842645523328034e-16, + "reduced_integral": 0.0002591141546920211, + "reduced_abs_error": 4.87890977618477e-19, + "reduced_rel_error": 1.8829190485497617e-15 + } + }, + { + "exponents": [ + 3, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0006285380457970661, + "original_integral": 0.0006285380457970658, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 5.174876110055988e-16, + "reduced_integral": 0.000628538045797064, + "reduced_abs_error": 2.0599841277224584e-18, + "reduced_rel_error": 3.2774215363687916e-15 + } + }, + { + "exponents": [ + 3, + 5, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0002966610561907728, + "original_integral": 0.00029666105619077283, + "original_abs_error": 5.421010862427522e-20, + "original_rel_error": 1.8273415904450404e-16, + "reduced_integral": 0.000296661056190772, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 2.5582782266230566e-15 + } + }, + { + "exponents": [ + 3, + 6, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00038160662302064976, + "original_integral": 0.00038160662302064976, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0003816066230206487, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 2.8411513508423447e-15 + } + }, + { + "exponents": [ + 4, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.008440467836218837, + "original_integral": 0.008440467836218851, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 1.6441965157741104e-15, + "reduced_integral": 0.00844046783621882, + "reduced_abs_error": 1.734723475976807e-17, + "reduced_rel_error": 2.055245644717638e-15 + } + }, + { + "exponents": [ + 4, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.003983781283854647, + "original_integral": 0.003983781283854649, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 4.3544646464584893e-16, + "reduced_integral": 0.003983781283854641, + "reduced_abs_error": 6.071532165918825e-18, + "reduced_rel_error": 1.5240626262604712e-15 + } + }, + { + "exponents": [ + 4, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0020561314271487006, + "original_integral": 0.0020561314271487037, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 1.4764455437409468e-15, + "reduced_integral": 0.0020561314271486967, + "reduced_abs_error": 3.903127820947816e-18, + "reduced_rel_error": 1.8982871276669317e-15 + } + }, + { + "exponents": [ + 4, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0011364558012803866, + "original_integral": 0.0011364558012803869, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.9080410716615452e-16, + "reduced_integral": 0.0011364558012803851, + "reduced_abs_error": 1.5178830414797062e-18, + "reduced_rel_error": 1.3356287501630816e-15 + } + }, + { + "exponents": [ + 4, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0006605031807901359, + "original_integral": 0.0006605031807901366, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 9.848874652096417e-16, + "reduced_integral": 0.0006605031807901346, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 1.9697749304192835e-15 + } + }, + { + "exponents": [ + 4, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00039815325297905344, + "original_integral": 0.00039815325297905344, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0003981532529790527, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 1.906154263619141e-15 + } + }, + { + "exponents": [ + 4, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.003865742269027963, + "original_integral": 0.0038657422690279637, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 2.243713309440313e-16, + "reduced_integral": 0.003865742269027953, + "reduced_abs_error": 9.974659986866641e-18, + "reduced_rel_error": 2.58027030585636e-15 + } + }, + { + "exponents": [ + 4, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.001824575603910886, + "original_integral": 0.0018245756039108856, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.3768862636584016e-16, + "reduced_integral": 0.001824575603910883, + "reduced_abs_error": 3.0357660829594124e-18, + "reduced_rel_error": 1.6638203845608812e-15 + } + }, + { + "exponents": [ + 4, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0009417101424755005, + "original_integral": 0.0009417101424755011, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 6.90787190399435e-16, + "reduced_integral": 0.0009417101424754985, + "reduced_abs_error": 1.951563910473908e-18, + "reduced_rel_error": 2.072361571198305e-15 + } + }, + { + "exponents": [ + 4, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00052049783414135, + "original_integral": 0.0005204978341413503, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 4.1660199192724053e-16, + "reduced_integral": 0.000520497834141349, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 2.0830099596362025e-15 + } + }, + { + "exponents": [ + 4, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0003025110828396557, + "original_integral": 0.000302511082839656, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 1.0752024312380446e-15, + "reduced_integral": 0.0003025110828396552, + "reduced_abs_error": 4.87890977618477e-19, + "reduced_rel_error": 1.612803646857067e-15 + } + }, + { + "exponents": [ + 4, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.001946356703161814, + "original_integral": 0.0019463567031618122, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 8.91266987782243e-16, + "reduced_integral": 0.0019463567031618094, + "reduced_abs_error": 4.553649124439119e-18, + "reduced_rel_error": 2.3395758429283878e-15 + } + }, + { + "exponents": [ + 4, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0009186527993731023, + "original_integral": 0.0009186527993731027, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 4.720835437394301e-16, + "reduced_integral": 0.0009186527993731008, + "reduced_abs_error": 1.5178830414797062e-18, + "reduced_rel_error": 1.6522924030880051e-15 + } + }, + { + "exponents": [ + 4, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0004741402092239166, + "original_integral": 0.0004741402092239167, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 2.2866699583655876e-16, + "reduced_integral": 0.0004741402092239158, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 1.6006689708559115e-15 + } + }, + { + "exponents": [ + 4, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0002620646639013932, + "original_integral": 0.00026206466390139334, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 6.205732716945709e-16, + "reduced_integral": 0.0002620646639013926, + "reduced_abs_error": 5.963111948670274e-19, + "reduced_rel_error": 2.2754353295467598e-15 + } + }, + { + "exponents": [ + 4, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0010525058093857175, + "original_integral": 0.0010525058093857169, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 6.180690858808387e-16, + "reduced_integral": 0.0010525058093857154, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 2.060230286269462e-15 + } + }, + { + "exponents": [ + 4, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0004967678363261753, + "original_integral": 0.000496767836326175, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 4.365025644589529e-16, + "reduced_integral": 0.0004967678363261743, + "reduced_abs_error": 9.75781955236954e-19, + "reduced_rel_error": 1.964261540065288e-15 + } + }, + { + "exponents": [ + 4, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00025639458782702047, + "original_integral": 0.0002563945878270209, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.6914587498500144e-15, + "reduced_integral": 0.00025639458782702014, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 1.2685940623875108e-15 + } + }, + { + "exponents": [ + 4, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0005992998752337373, + "original_integral": 0.0005992998752337378, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 7.23645852295662e-16, + "reduced_integral": 0.0005992998752337359, + "reduced_abs_error": 1.4094628242311558e-18, + "reduced_rel_error": 2.3518490199609016e-15 + } + }, + { + "exponents": [ + 4, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00028286105375909253, + "original_integral": 0.0002828610537590928, + "original_abs_error": 2.710505431213761e-19, + "original_rel_error": 9.582462467675906e-16, + "reduced_integral": 0.000282861053759092, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 1.916492493535181e-15 + } + }, + { + "exponents": [ + 4, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00035413509860680897, + "original_integral": 0.0003541350986068095, + "original_abs_error": 5.421010862427522e-19, + "original_rel_error": 1.530774804235485e-15, + "reduced_integral": 0.0003541350986068079, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 3.06154960847097e-15 + } + }, + { + "exponents": [ + 5, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0049366547789406945, + "original_integral": 0.004936654778940692, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 5.270948305045477e-16, + "reduced_integral": 0.004936654778940685, + "reduced_abs_error": 9.540979117872439e-18, + "reduced_rel_error": 1.9326810451833414e-15 + } + }, + { + "exponents": [ + 5, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00233003114220808, + "original_integral": 0.002330031142208081, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 3.7225328120139994e-16, + "reduced_integral": 0.0023300311422080762, + "reduced_abs_error": 3.903127820947816e-18, + "reduced_rel_error": 1.6751397654062997e-15 + } + }, + { + "exponents": [ + 5, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0012025886755242907, + "original_integral": 0.0012025886755242922, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 1.2621797231027118e-15, + "reduced_integral": 0.0012025886755242874, + "reduced_abs_error": 3.2526065174565133e-18, + "reduced_rel_error": 2.704670835220097e-15 + } + }, + { + "exponents": [ + 5, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0006646894545787399, + "original_integral": 0.0006646894545787413, + "original_abs_error": 1.4094628242311558e-18, + "original_rel_error": 2.120483203881173e-15, + "reduced_integral": 0.0006646894545787382, + "reduced_abs_error": 1.6263032587282567e-18, + "reduced_rel_error": 2.446711389093661e-15 + } + }, + { + "exponents": [ + 5, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0003863146270117028, + "original_integral": 0.00038631462701170294, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 4.2097894954388835e-16, + "reduced_integral": 0.0003863146270117019, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 2.2452210642340713e-15 + } + }, + { + "exponents": [ + 5, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0022609925678123447, + "original_integral": 0.0022609925678123474, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 1.1508596936622816e-15, + "reduced_integral": 0.0022609925678123387, + "reduced_abs_error": 6.071532165918825e-18, + "reduced_rel_error": 2.6853392852119907e-15 + } + }, + { + "exponents": [ + 5, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0010671564715801782, + "original_integral": 0.0010671564715801784, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 2.0319460198373458e-16, + "reduced_integral": 0.001067156471580177, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 1.2191676119024076e-15 + } + }, + { + "exponents": [ + 5, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0005507867532270834, + "original_integral": 0.0005507867532270835, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 1.968460871894825e-16, + "reduced_integral": 0.000550786753227083, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 7.8738434875793e-16 + } + }, + { + "exponents": [ + 5, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.000304428400202668, + "original_integral": 0.00030442840020266835, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 1.0684307099111468e-15, + "reduced_integral": 0.00030442840020266753, + "reduced_abs_error": 4.87890977618477e-19, + "reduced_rel_error": 1.6026460648667204e-15 + } + }, + { + "exponents": [ + 5, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0011383837136320907, + "original_integral": 0.001138383713632092, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 1.1428858225945102e-15, + "reduced_integral": 0.001138383713632088, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 2.2857716451890203e-15 + } + }, + { + "exponents": [ + 5, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0005373009909180692, + "original_integral": 0.0005373009909180693, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 2.0178674352209226e-16, + "reduced_integral": 0.0005373009909180676, + "reduced_abs_error": 1.5178830414797062e-18, + "reduced_rel_error": 2.8250144093092917e-15 + } + }, + { + "exponents": [ + 5, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.000277314785764501, + "original_integral": 0.0002773147857645012, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 5.864466455493407e-16, + "reduced_integral": 0.0002773147857645005, + "reduced_abs_error": 4.87890977618477e-19, + "reduced_rel_error": 1.759339936648022e-15 + } + }, + { + "exponents": [ + 5, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0006155888434845912, + "original_integral": 0.000615588843484592, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 1.2328708175473134e-15, + "reduced_integral": 0.0006155888434845895, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.8179904401081447e-15 + } + }, + { + "exponents": [ + 5, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0002905492160873225, + "original_integral": 0.0002905492160873226, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 3.731561169174365e-16, + "reduced_integral": 0.00029054921608732184, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 2.238936701504619e-15 + } + }, + { + "exponents": [ + 5, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00035051808152100703, + "original_integral": 0.00035051808152100655, + "original_abs_error": 4.87890977618477e-19, + "original_rel_error": 1.3919138650461802e-15, + "reduced_integral": 0.00035051808152100606, + "reduced_abs_error": 9.75781955236954e-19, + "reduced_rel_error": 2.7838277300923604e-15 + } + }, + { + "exponents": [ + 6, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0029669160778858826, + "original_integral": 0.00296691607788588, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 8.770336422253516e-16, + "reduced_integral": 0.002966916077885876, + "reduced_abs_error": 6.5052130349130266e-18, + "reduced_rel_error": 2.192584105563379e-15 + } + }, + { + "exponents": [ + 6, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0014003423709679262, + "original_integral": 0.0014003423709679273, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 7.742407820853813e-16, + "reduced_integral": 0.0014003423709679232, + "reduced_abs_error": 3.0357660829594124e-18, + "reduced_rel_error": 2.167874189839068e-15 + } + }, + { + "exponents": [ + 6, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0007227525189156776, + "original_integral": 0.0007227525189156776, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.000722752518915675, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 3.60024371533016e-15 + } + }, + { + "exponents": [ + 6, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00039947655201728096, + "original_integral": 0.00039947655201728156, + "original_abs_error": 5.963111948670274e-19, + "original_rel_error": 1.4927314052746495e-15, + "reduced_integral": 0.00039947655201727966, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 3.256868520599235e-15 + } + }, + { + "exponents": [ + 6, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0013588503757725524, + "original_integral": 0.0013588503757725515, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 6.383055511135867e-16, + "reduced_integral": 0.0013588503757725487, + "reduced_abs_error": 3.686287386450715e-18, + "reduced_rel_error": 2.712798592232744e-15 + } + }, + { + "exponents": [ + 6, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0006413581331750713, + "original_integral": 0.0006413581331750718, + "original_abs_error": 5.421010862427522e-19, + "original_rel_error": 8.452392792760844e-16, + "reduced_integral": 0.0006413581331750708, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 8.452392792760844e-16 + } + }, + { + "exponents": [ + 6, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00033102133870228825, + "original_integral": 0.00033102133870228847, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 6.550648225494653e-16, + "reduced_integral": 0.00033102133870228754, + "reduced_abs_error": 7.047314121155779e-19, + "reduced_rel_error": 2.128960673285762e-15 + } + }, + { + "exponents": [ + 6, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0006841655116712915, + "original_integral": 0.0006841655116712926, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 1.5847074340783363e-15, + "reduced_integral": 0.0006841655116712898, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.535531894525338e-15 + } + }, + { + "exponents": [ + 6, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00032291643228107234, + "original_integral": 0.00032291643228107283, + "original_abs_error": 4.87890977618477e-19, + "original_rel_error": 1.510889285416754e-15, + "reduced_integral": 0.00032291643228107175, + "reduced_abs_error": 5.963111948670274e-19, + "reduced_rel_error": 1.84664245995381e-15 + } + }, + { + "exponents": [ + 6, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00036996721846803264, + "original_integral": 0.0003699672184680329, + "original_abs_error": 2.710505431213761e-19, + "original_rel_error": 7.326339459040382e-16, + "reduced_integral": 0.00036996721846803166, + "reduced_abs_error": 9.75781955236954e-19, + "reduced_rel_error": 2.6374822052545374e-15 + } + }, + { + "exponents": [ + 7, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0018197428512164628, + "original_integral": 0.001819742851216463, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.1915993204872175e-16, + "reduced_integral": 0.001819742851216458, + "reduced_abs_error": 4.7704895589362195e-18, + "reduced_rel_error": 2.6215185050718786e-15 + } + }, + { + "exponents": [ + 7, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0008588928543742958, + "original_integral": 0.0008588928543742973, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 1.7672554076439317e-15, + "reduced_integral": 0.0008588928543742947, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 1.262325291174237e-15 + } + }, + { + "exponents": [ + 7, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00044329657292924705, + "original_integral": 0.0004432965729292479, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.9566172872869018e-15, + "reduced_integral": 0.000443296572929246, + "reduced_abs_error": 1.0299920638612292e-18, + "reduced_rel_error": 2.323483028653196e-15 + } + }, + { + "exponents": [ + 7, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0008334439506448411, + "original_integral": 0.0008334439506448407, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 3.9026097855050124e-16, + "reduced_integral": 0.0008334439506448391, + "reduced_abs_error": 1.951563910473908e-18, + "reduced_rel_error": 2.3415658713030076e-15 + } + }, + { + "exponents": [ + 7, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0003933737413787957, + "original_integral": 0.00039337374137879584, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 4.1342445813184633e-16, + "reduced_integral": 0.00039337374137879503, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 1.6536978325273853e-15 + } + }, + { + "exponents": [ + 7, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.00041962942875008186, + "original_integral": 0.00041962942875008186, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.000419629428750081, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 2.0669707093040346e-15 + } + }, + { + "exponents": [ + 8, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0011337546528181915, + "original_integral": 0.001133754652818191, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 3.8251738849864434e-16, + "reduced_integral": 0.0011337546528181893, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 1.9125869424932217e-15 + } + }, + { + "exponents": [ + 8, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0005351161397711807, + "original_integral": 0.0005351161397711814, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 1.2156637693071078e-15, + "reduced_integral": 0.0005351161397711794, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 2.4313275386142156e-15 + } + }, + { + "exponents": [ + 8, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0005192607055854634, + "original_integral": 0.0005192607055854634, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0005192607055854621, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 2.5055672285382876e-15 + } + }, + { + "exponents": [ + 9, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 9, + "original_points": 486, + "reduced_points": 220, + "exact_integral": 0.0007151762907816549, + "original_integral": 0.0007151762907816549, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0007151762907816536, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 1.8191914689462444e-15 + } + } + ] + }, + { + "strength": 10, + "results": [ + { + "exponents": [ + 0, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.12499999999999997, + "original_integral": 0.125, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 2.2204460492503136e-16, + "reduced_integral": 0.12499999999999992, + "reduced_abs_error": 5.551115123125783e-17, + "reduced_rel_error": 4.440892098500627e-16 + } + }, + { + "exponents": [ + 0, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.05899822973615082, + "original_integral": 0.058998229736150876, + "original_abs_error": 5.551115123125783e-17, + "original_rel_error": 9.408952010850538e-16, + "reduced_integral": 0.058998229736150834, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 2.3522380027126344e-16 + } + }, + { + "exponents": [ + 0, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.030450495562663715, + "original_integral": 0.030450495562663757, + "original_abs_error": 4.163336342344337e-17, + "original_rel_error": 1.367247483305044e-15, + "reduced_integral": 0.030450495562663698, + "reduced_abs_error": 1.734723475976807e-17, + "reduced_rel_error": 5.696864513771016e-16 + } + }, + { + "exponents": [ + 0, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.016830462234625018, + "original_integral": 0.016830462234625046, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 1.649127351863685e-15, + "reduced_integral": 0.016830462234625004, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 8.245636759318425e-16 + } + }, + { + "exponents": [ + 0, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00978179162587195, + "original_integral": 0.00978179162587195, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.009781791625871948, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 1.7734210074447112e-16 + } + }, + { + "exponents": [ + 0, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0058964926575299035, + "original_integral": 0.005896492657529905, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.941958171967773e-16, + "reduced_integral": 0.005896492657529893, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 1.7651749031806639e-15 + } + }, + { + "exponents": [ + 0, + 0, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0036511518202430055, + "original_integral": 0.0036511518202430085, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 8.314543553429571e-16, + "reduced_integral": 0.003651151820243002, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 9.502335489633796e-16 + } + }, + { + "exponents": [ + 0, + 0, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.002306984964566927, + "original_integral": 0.0023069849645669313, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 1.879859971586826e-15, + "reduced_integral": 0.002306984964566923, + "reduced_abs_error": 3.903127820947816e-18, + "reduced_rel_error": 1.6918739744281432e-15 + } + }, + { + "exponents": [ + 0, + 0, + 8 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.001480624539659955, + "original_integral": 0.001480624539659957, + "original_abs_error": 1.951563910473908e-18, + "original_rel_error": 1.3180680572281413e-15, + "reduced_integral": 0.0014806245396599524, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 1.7574240763041884e-15 + } + }, + { + "exponents": [ + 0, + 0, + 9 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.000962107316018366, + "original_integral": 0.0009621073160183683, + "original_abs_error": 2.2768245622195593e-18, + "original_rel_error": 2.3664975042930617e-15, + "reduced_integral": 0.0009621073160183636, + "reduced_abs_error": 2.3852447794681098e-18, + "reduced_rel_error": 2.4791878616403506e-15 + } + }, + { + "exponents": [ + 0, + 0, + 10 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0006314831716689477, + "original_integral": 0.0006314831716689485, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.3735310407339156e-15, + "reduced_integral": 0.0006314831716689458, + "reduced_abs_error": 1.8431436932253575e-18, + "reduced_rel_error": 2.9187534615595706e-15 + } + }, + { + "exponents": [ + 0, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.057250118477433484, + "original_integral": 0.05725011847743346, + "original_abs_error": 2.0816681711721685e-17, + "original_rel_error": 3.6360940842292024e-16, + "reduced_integral": 0.0572501184774335, + "reduced_abs_error": 1.3877787807814457e-17, + "reduced_rel_error": 2.4240627228194682e-16 + } + }, + { + "exponents": [ + 0, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.027021245138827792, + "original_integral": 0.02702124513882782, + "original_abs_error": 2.7755575615628914e-17, + "original_rel_error": 1.027176041408467e-15, + "reduced_integral": 0.027021245138827796, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 1.2839700517605837e-16 + } + }, + { + "exponents": [ + 0, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.013946355829272483, + "original_integral": 0.01394635582927249, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 4.975417226443446e-16, + "reduced_integral": 0.013946355829272486, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 2.487708613221723e-16 + } + }, + { + "exponents": [ + 0, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0077083676556980165, + "original_integral": 0.0077083676556980165, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.007708367655698008, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 1.1252210282773043e-15 + } + }, + { + "exponents": [ + 0, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.004480069836021888, + "original_integral": 0.004480069836021894, + "original_abs_error": 6.071532165918825e-18, + "original_rel_error": 1.3552315897178265e-15, + "reduced_integral": 0.004480069836021884, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 7.744180512673294e-16 + } + }, + { + "exponents": [ + 0, + 1, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.002700599225959228, + "original_integral": 0.002700599225959227, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 3.211738082611368e-16, + "reduced_integral": 0.0027005992259592267, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 4.817607123917052e-16 + } + }, + { + "exponents": [ + 0, + 1, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0016722309943040716, + "original_integral": 0.001672230994304074, + "original_abs_error": 2.3852447794681098e-18, + "original_rel_error": 1.4263847444478037e-15, + "reduced_integral": 0.0016722309943040705, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 6.48356702021729e-16 + } + }, + { + "exponents": [ + 0, + 1, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.001056601300376914, + "original_integral": 0.001056601300376916, + "original_abs_error": 1.951563910473908e-18, + "original_rel_error": 1.8470201671886454e-15, + "reduced_integral": 0.0010566013003769127, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 1.2313467781257637e-15 + } + }, + { + "exponents": [ + 0, + 1, + 8 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0006781274425290227, + "original_integral": 0.0006781274425290236, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.2790541771228819e-15, + "reduced_integral": 0.0006781274425290213, + "reduced_abs_error": 1.4094628242311558e-18, + "reduced_rel_error": 2.078463037824683e-15 + } + }, + { + "exponents": [ + 0, + 1, + 9 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00044064606264045586, + "original_integral": 0.0004406460626404569, + "original_abs_error": 1.0299920638612292e-18, + "original_rel_error": 2.3374589067907976e-15, + "reduced_integral": 0.00044064606264045516, + "reduced_abs_error": 7.047314121155779e-19, + "reduced_rel_error": 1.5993139888568615e-15 + } + }, + { + "exponents": [ + 0, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.02882477519210804, + "original_integral": 0.028824775192108076, + "original_abs_error": 3.469446951953614e-17, + "original_rel_error": 1.2036336550175477e-15, + "reduced_integral": 0.028824775192108052, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 3.6109009650526434e-16 + } + }, + { + "exponents": [ + 0, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.013604885671015126, + "original_integral": 0.013604885671015147, + "original_abs_error": 2.0816681711721685e-17, + "original_rel_error": 1.5300886913052942e-15, + "reduced_integral": 0.013604885671015131, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 3.8252217282632356e-16 + } + }, + { + "exponents": [ + 0, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.007021829512656519, + "original_integral": 0.007021829512656529, + "original_abs_error": 9.540979117872439e-18, + "original_rel_error": 1.3587597221885365e-15, + "reduced_integral": 0.007021829512656519, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 0, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.003881074322338644, + "original_integral": 0.003881074322338643, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 2.234849595628851e-16, + "reduced_integral": 0.0038810743223386414, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 6.704548786886553e-16 + } + }, + { + "exponents": [ + 0, + 2, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0022556635567344318, + "original_integral": 0.002255663556734433, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 5.767893013558059e-16, + "reduced_integral": 0.002255663556734431, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 3.8452620090387066e-16 + } + }, + { + "exponents": [ + 0, + 2, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0013597206022017202, + "original_integral": 0.001359720602201722, + "original_abs_error": 1.951563910473908e-18, + "original_rel_error": 1.4352683244733137e-15, + "reduced_integral": 0.0013597206022017193, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 6.378970330992505e-16 + } + }, + { + "exponents": [ + 0, + 2, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0008419490432860857, + "original_integral": 0.000841949043286086, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 3.8631869035229855e-16, + "reduced_integral": 0.0008419490432860844, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 1.5452747614091942e-15 + } + }, + { + "exponents": [ + 0, + 2, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00053198658380172, + "original_integral": 0.0005319865838017204, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 8.152101616830278e-16, + "reduced_integral": 0.0005319865838017192, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 1.4266177829452987e-15 + } + }, + { + "exponents": [ + 0, + 2, + 8 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00034142935599693324, + "original_integral": 0.000341429355996934, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 2.222836166280529e-15, + "reduced_integral": 0.0003414293559969325, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 2.222836166280529e-15 + } + }, + { + "exponents": [ + 0, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.015587195961894966, + "original_integral": 0.015587195961894949, + "original_abs_error": 1.734723475976807e-17, + "original_rel_error": 1.1129156778535254e-15, + "reduced_integral": 0.015587195961894975, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 5.564578389267627e-16 + } + }, + { + "exponents": [ + 0, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.007356935746418251, + "original_integral": 0.0073569357464182555, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 5.894857369188534e-16, + "reduced_integral": 0.007356935746418258, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 9.431771790701655e-16 + } + }, + { + "exponents": [ + 0, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0037971027317764182, + "original_integral": 0.003797102731776424, + "original_abs_error": 5.637851296924623e-18, + "original_rel_error": 1.4847771301375978e-15, + "reduced_integral": 0.00379710273177642, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 4.568545015807994e-16 + } + }, + { + "exponents": [ + 0, + 3, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.002098717703842982, + "original_integral": 0.0020987177038429843, + "original_abs_error": 2.168404344971009e-18, + "original_rel_error": 1.0332043899951017e-15, + "reduced_integral": 0.0020987177038429825, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 2.0664087799902035e-16 + } + }, + { + "exponents": [ + 0, + 3, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.001219765623447114, + "original_integral": 0.001219765623447115, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 7.110888529037235e-16, + "reduced_integral": 0.0012197656234471136, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 3.5554442645186176e-16 + } + }, + { + "exponents": [ + 0, + 3, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0007352782923263471, + "original_integral": 0.0007352782923263479, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.1796373523338458e-15, + "reduced_integral": 0.0007352782923263463, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 1.032182683292115e-15 + } + }, + { + "exponents": [ + 0, + 3, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0004552897512700577, + "original_integral": 0.00045528975127005815, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 9.525381754902747e-16, + "reduced_integral": 0.0004552897512700571, + "reduced_abs_error": 5.963111948670274e-19, + "reduced_rel_error": 1.3097399912991278e-15 + } + }, + { + "exponents": [ + 0, + 3, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0002876754137908, + "original_integral": 0.0002876754137908002, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 7.537676982531051e-16, + "reduced_integral": 0.0002876754137907997, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 1.1306515473796576e-15 + } + }, + { + "exponents": [ + 0, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.008875394807235764, + "original_integral": 0.008875394807235766, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.9545310531567051e-16, + "reduced_integral": 0.00887539480723577, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 5.863593159470116e-16 + } + }, + { + "exponents": [ + 0, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.004189060654690686, + "original_integral": 0.004189060654690683, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 6.211619808014799e-16, + "reduced_integral": 0.004189060654690687, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 2.070539936004933e-16 + } + }, + { + "exponents": [ + 0, + 4, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00216208136155697, + "original_integral": 0.0021620813615569707, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 4.01169795647188e-16, + "reduced_integral": 0.0021620813615569707, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 4.01169795647188e-16 + } + }, + { + "exponents": [ + 0, + 4, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0011950159769645481, + "original_integral": 0.0011950159769645499, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.4516320362370101e-15, + "reduced_integral": 0.0011950159769645485, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 3.6290800905925253e-16 + } + }, + { + "exponents": [ + 0, + 4, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0006945381008138094, + "original_integral": 0.0006945381008138092, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 3.1220811967410134e-16, + "reduced_integral": 0.000694538100813809, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 6.244162393482027e-16 + } + }, + { + "exponents": [ + 0, + 4, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00041866960250835743, + "original_integral": 0.00041866960250835797, + "original_abs_error": 5.421010862427522e-19, + "original_rel_error": 1.2948183555598137e-15, + "reduced_integral": 0.00041866960250835683, + "reduced_abs_error": 5.963111948670274e-19, + "reduced_rel_error": 1.424300191115795e-15 + } + }, + { + "exponents": [ + 0, + 4, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0002592433112465134, + "original_integral": 0.0002592433112465136, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 8.364359853855909e-16, + "reduced_integral": 0.00025924331124651276, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 2.5093079561567727e-15 + } + }, + { + "exponents": [ + 0, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.005244601150649224, + "original_integral": 0.005244601150649229, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 9.922909823726447e-16, + "reduced_integral": 0.005244601150649236, + "reduced_abs_error": 1.214306433183765e-17, + "reduced_rel_error": 2.315345625536171e-15 + } + }, + { + "exponents": [ + 0, + 5, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.002475377468483872, + "original_integral": 0.0024753774684838697, + "original_abs_error": 2.168404344971009e-18, + "original_rel_error": 8.759893683201055e-16, + "reduced_integral": 0.0024753774684838754, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 1.401582989312169e-15 + } + }, + { + "exponents": [ + 0, + 5, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.001277605632526282, + "original_integral": 0.001277605632526281, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 8.486203761810678e-16, + "reduced_integral": 0.0012776056325262822, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 1.6972407523621356e-16 + } + }, + { + "exponents": [ + 0, + 5, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0007061524928133816, + "original_integral": 0.0007061524928133826, + "original_abs_error": 9.75781955236954e-19, + "original_rel_error": 1.3818289465343978e-15, + "reduced_integral": 0.0007061524928133811, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 7.676827480746654e-16 + } + }, + { + "exponents": [ + 0, + 5, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00041041276493167196, + "original_integral": 0.00041041276493167256, + "original_abs_error": 5.963111948670274e-19, + "original_rel_error": 1.4529547953175993e-15, + "reduced_integral": 0.0004104127649316716, + "reduced_abs_error": 3.7947076036992655e-19, + "reduced_rel_error": 9.246075970202904e-16 + } + }, + { + "exponents": [ + 0, + 5, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00024739801741180823, + "original_integral": 0.00024739801741180856, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 1.314726185554819e-15, + "reduced_integral": 0.0002473980174118079, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 1.314726185554819e-15 + } + }, + { + "exponents": [ + 0, + 6, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0031841740489256596, + "original_integral": 0.0031841740489256583, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 4.0859657386554515e-16, + "reduced_integral": 0.0031841740489256627, + "reduced_abs_error": 3.0357660829594124e-18, + "reduced_rel_error": 9.53392005686272e-16 + } + }, + { + "exponents": [ + 0, + 6, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0015028850564672448, + "original_integral": 0.0015028850564672465, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.1542622428188441e-15, + "reduced_integral": 0.0015028850564672467, + "reduced_abs_error": 1.951563910473908e-18, + "reduced_rel_error": 1.2985450231711996e-15 + } + }, + { + "exponents": [ + 0, + 6, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0007756774219804779, + "original_integral": 0.0007756774219804781, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 2.795497565772364e-16, + "reduced_integral": 0.000775677421980478, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 1.397748782886182e-16 + } + }, + { + "exponents": [ + 0, + 6, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0004287289686313307, + "original_integral": 0.00042872896863133087, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 3.7933132065231046e-16, + "reduced_integral": 0.0004287289686313308, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 2.528875471015403e-16 + } + }, + { + "exponents": [ + 0, + 6, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00024917541637679835, + "original_integral": 0.00024917541637679846, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 4.3511602719507226e-16, + "reduced_integral": 0.0002491754163767982, + "reduced_abs_error": 1.6263032587282567e-19, + "reduced_rel_error": 6.526740407926084e-16 + } + }, + { + "exponents": [ + 0, + 7, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.001972861658829849, + "original_integral": 0.0019728616588298494, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.198232537254681e-16, + "reduced_integral": 0.0019728616588298533, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 2.198232537254681e-15 + } + }, + { + "exponents": [ + 0, + 7, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0009311627630822963, + "original_integral": 0.0009311627630822967, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 4.657412067882197e-16, + "reduced_integral": 0.0009311627630822981, + "reduced_abs_error": 1.8431436932253575e-18, + "reduced_rel_error": 1.9794001288499336e-15 + } + }, + { + "exponents": [ + 0, + 7, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0004805969215035816, + "original_integral": 0.00048059692150358233, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 1.5791643408065344e-15, + "reduced_integral": 0.00048059692150358185, + "reduced_abs_error": 2.710505431213761e-19, + "reduced_rel_error": 5.639872645737622e-16 + } + }, + { + "exponents": [ + 0, + 7, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0002656333891446035, + "original_integral": 0.00026563338914460357, + "original_abs_error": 5.421010862427522e-20, + "original_rel_error": 2.0407866947315394e-16, + "reduced_integral": 0.00026563338914460357, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 2.0407866947315394e-16 + } + }, + { + "exponents": [ + 0, + 8, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0012416379281119064, + "original_integral": 0.001241637928111907, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 5.239219008721135e-16, + "reduced_integral": 0.0012416379281119088, + "reduced_abs_error": 2.3852447794681098e-18, + "reduced_rel_error": 1.921046969864416e-15 + } + }, + { + "exponents": [ + 0, + 8, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0005860355178549166, + "original_integral": 0.0005860355178549174, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.4800497778073825e-15, + "reduced_integral": 0.0005860355178549173, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 1.2950435555814597e-15 + } + }, + { + "exponents": [ + 0, + 8, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0003024679217632526, + "original_integral": 0.00030246792176325313, + "original_abs_error": 5.421010862427522e-19, + "original_rel_error": 1.7922597645480735e-15, + "reduced_integral": 0.00030246792176325297, + "reduced_abs_error": 3.7947076036992655e-19, + "reduced_rel_error": 1.2545818351836516e-15 + } + }, + { + "exponents": [ + 0, + 9, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0007911818565258345, + "original_integral": 0.0007911818565258339, + "original_abs_error": 5.421010862427522e-19, + "original_rel_error": 6.851788647216671e-16, + "reduced_integral": 0.0007911818565258375, + "reduced_abs_error": 3.0357660829594124e-18, + "reduced_rel_error": 3.837001642441336e-15 + } + }, + { + "exponents": [ + 0, + 9, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0003734266314750841, + "original_integral": 0.00037342663147508414, + "original_abs_error": 5.421010862427522e-20, + "original_rel_error": 1.451693694425012e-16, + "reduced_integral": 0.0003734266314750848, + "reduced_abs_error": 7.047314121155779e-19, + "reduced_rel_error": 1.8872018027525152e-15 + } + }, + { + "exponents": [ + 0, + 10, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0005092358173789841, + "original_integral": 0.000509235817378984, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 2.1290768156604708e-16, + "reduced_integral": 0.000509235817378986, + "reduced_abs_error": 1.951563910473908e-18, + "reduced_rel_error": 3.832338268188848e-15 + } + }, + { + "exponents": [ + 1, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.056360767612003086, + "original_integral": 0.05636076761200307, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 2.4623134843285776e-16, + "reduced_integral": 0.05636076761200306, + "reduced_abs_error": 2.7755575615628914e-17, + "reduced_rel_error": 4.924626968657155e-16 + } + }, + { + "exponents": [ + 1, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.02660148412543013, + "original_integral": 0.026601484125430137, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 2.6084611938150766e-16, + "reduced_integral": 0.026601484125430102, + "reduced_abs_error": 2.7755575615628914e-17, + "reduced_rel_error": 1.0433844775260306e-15 + } + }, + { + "exponents": [ + 1, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.013729706432620965, + "original_integral": 0.013729706432620954, + "original_abs_error": 1.0408340855860843e-17, + "original_rel_error": 7.580891045952188e-16, + "reduced_integral": 0.013729706432620956, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 6.317409204960156e-16 + } + }, + { + "exponents": [ + 1, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.007588622166466357, + "original_integral": 0.007588622166466358, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.1429765759339295e-16, + "reduced_integral": 0.007588622166466353, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 5.714882879669648e-16 + } + }, + { + "exponents": [ + 1, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.004410474277238454, + "original_integral": 0.004410474277238458, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 9.832975814695012e-16, + "reduced_integral": 0.004410474277238447, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 1.573276130351202e-15 + } + }, + { + "exponents": [ + 1, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0026586468191754017, + "original_integral": 0.0026586468191754043, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 9.78725415951362e-16, + "reduced_integral": 0.002658646819175397, + "reduced_abs_error": 4.7704895589362195e-18, + "reduced_rel_error": 1.7943299292441637e-15 + } + }, + { + "exponents": [ + 1, + 0, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0016462537540548646, + "original_integral": 0.0016462537540548675, + "original_abs_error": 2.8189256484623115e-18, + "original_rel_error": 1.7123275445957557e-15, + "reduced_integral": 0.0016462537540548605, + "reduced_abs_error": 4.119968255444917e-18, + "reduced_rel_error": 2.502632565178412e-15 + } + }, + { + "exponents": [ + 1, + 0, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.001040187547778734, + "original_integral": 0.0010401875477787355, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 1.459239773366895e-15, + "reduced_integral": 0.0010401875477787316, + "reduced_abs_error": 2.3852447794681098e-18, + "reduced_rel_error": 2.2930910724336925e-15 + } + }, + { + "exponents": [ + 1, + 0, + 8 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0006675930848032302, + "original_integral": 0.0006675930848032313, + "original_abs_error": 1.1926223897340549e-18, + "original_rel_error": 1.786451083575221e-15, + "reduced_integral": 0.0006675930848032282, + "reduced_abs_error": 1.951563910473908e-18, + "reduced_rel_error": 2.923283591304907e-15 + } + }, + { + "exponents": [ + 1, + 0, + 9 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0004338008548473529, + "original_integral": 0.00043380085484735363, + "original_abs_error": 7.047314121155779e-19, + "original_rel_error": 1.6245505379733767e-15, + "reduced_integral": 0.00043380085484735173, + "reduced_abs_error": 1.1926223897340549e-18, + "reduced_rel_error": 2.749239371954945e-15 + } + }, + { + "exponents": [ + 1, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.02581328498613018, + "original_integral": 0.025813284986130173, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 2.6881095945888283e-16, + "reduced_integral": 0.02581328498613017, + "reduced_abs_error": 1.0408340855860843e-17, + "reduced_rel_error": 4.032164391883242e-16 + } + }, + { + "exponents": [ + 1, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.01218350494285153, + "original_integral": 0.012183504942851536, + "original_abs_error": 5.204170427930421e-18, + "original_rel_error": 4.271488748386713e-16, + "reduced_integral": 0.012183504942851538, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 5.695318331182284e-16 + } + }, + { + "exponents": [ + 1, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.006288218559423448, + "original_integral": 0.006288218559423449, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.758688266929227e-16, + "reduced_integral": 0.006288218559423451, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 5.517376533858454e-16 + } + }, + { + "exponents": [ + 1, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0034755961448854167, + "original_integral": 0.003475596144885417, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.2477884394951743e-16, + "reduced_integral": 0.0034755961448854136, + "reduced_abs_error": 3.0357660829594124e-18, + "reduced_rel_error": 8.73451907646622e-16 + } + }, + { + "exponents": [ + 1, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.002020001399308595, + "original_integral": 0.002020001399308598, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 1.502853455447354e-15, + "reduced_integral": 0.0020200013993085927, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 1.0734667538909673e-15 + } + }, + { + "exponents": [ + 1, + 1, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0012176627630995475, + "original_integral": 0.0012176627630995486, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 8.903960976236797e-16, + "reduced_integral": 0.0012176627630995467, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 7.123168780989439e-16 + } + }, + { + "exponents": [ + 1, + 1, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0007539857797084852, + "original_integral": 0.0007539857797084867, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 2.013145449595307e-15, + "reduced_integral": 0.0007539857797084842, + "reduced_abs_error": 9.75781955236954e-19, + "reduced_rel_error": 1.2941649318826972e-15 + } + }, + { + "exponents": [ + 1, + 1, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0004764068827926682, + "original_integral": 0.0004764068827926688, + "original_abs_error": 5.963111948670274e-19, + "original_rel_error": 1.2516846762823565e-15, + "reduced_integral": 0.00047640688279266717, + "reduced_abs_error": 1.0299920638612292e-18, + "reduced_rel_error": 2.1620008044877065e-15 + } + }, + { + "exponents": [ + 1, + 1, + 8 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00030575826559760185, + "original_integral": 0.0003057582655976025, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 2.1275673520055606e-15, + "reduced_integral": 0.00030575826559760115, + "reduced_abs_error": 7.047314121155779e-19, + "reduced_rel_error": 2.3048646313393573e-15 + } + }, + { + "exponents": [ + 1, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.012996691648565066, + "original_integral": 0.012996691648565059, + "original_abs_error": 6.938893903907228e-18, + "original_rel_error": 5.338969402011884e-16, + "reduced_integral": 0.012996691648565059, + "reduced_abs_error": 6.938893903907228e-18, + "reduced_rel_error": 5.338969402011884e-16 + } + }, + { + "exponents": [ + 1, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.006134254397535635, + "original_integral": 0.006134254397535638, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 4.241893220161472e-16, + "reduced_integral": 0.006134254397535636, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 1.4139644067204908e-16 + } + }, + { + "exponents": [ + 1, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.003166045610991512, + "original_integral": 0.0031660456109915158, + "original_abs_error": 3.903127820947816e-18, + "original_rel_error": 1.2328084622019933e-15, + "reduced_integral": 0.0031660456109915106, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 4.109361540673311e-16 + } + }, + { + "exponents": [ + 1, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.001749922623729925, + "original_integral": 0.0017499226237299257, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 3.717428957542874e-16, + "reduced_integral": 0.0017499226237299244, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 3.717428957542874e-16 + } + }, + { + "exponents": [ + 1, + 2, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0010170474362557894, + "original_integral": 0.001017047436255791, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 1.4924407528794515e-15, + "reduced_integral": 0.0010170474362557883, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 1.0660291091996082e-15 + } + }, + { + "exponents": [ + 1, + 2, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0006130791750235527, + "original_integral": 0.0006130791750235534, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 1.0610722562323399e-15, + "reduced_integral": 0.0006130791750235522, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 8.842268801936166e-16 + } + }, + { + "exponents": [ + 1, + 2, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0003796231549583632, + "original_integral": 0.0003796231549583636, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.142398358292364e-15, + "reduced_integral": 0.00037962315495836243, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 1.9991971270116367e-15 + } + }, + { + "exponents": [ + 1, + 2, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00023986537777881715, + "original_integral": 0.00023986537777881745, + "original_abs_error": 2.981555974335137e-19, + "original_rel_error": 1.243012227085339e-15, + "reduced_integral": 0.00023986537777881674, + "reduced_abs_error": 4.0657581468206416e-19, + "reduced_rel_error": 1.6950166732981897e-15 + } + }, + { + "exponents": [ + 1, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.007028050634648921, + "original_integral": 0.007028050634648921, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.007028050634648925, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 4.936570796529167e-16 + } + }, + { + "exponents": [ + 1, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.003317140367522541, + "original_integral": 0.0033171403675225425, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 3.9221813454771275e-16, + "reduced_integral": 0.003317140367522543, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 5.229575127302837e-16 + } + }, + { + "exponents": [ + 1, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0017120609973164226, + "original_integral": 0.0017120609973164224, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.2665461968760946e-16, + "reduced_integral": 0.001712060997316424, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 7.599277181256568e-16 + } + }, + { + "exponents": [ + 1, + 3, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.000946282726315928, + "original_integral": 0.0009462827263159287, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 8.020240670508353e-16, + "reduced_integral": 0.000946282726315928, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 1, + 3, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0005499754147537028, + "original_integral": 0.0005499754147537029, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 1.9713647981356513e-16, + "reduced_integral": 0.0005499754147537026, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 3.9427295962713025e-16 + } + }, + { + "exponents": [ + 1, + 3, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00033152679171164574, + "original_integral": 0.00033152679171164607, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 9.810991445558808e-16, + "reduced_integral": 0.00033152679171164547, + "reduced_abs_error": 2.710505431213761e-19, + "reduced_rel_error": 8.17582620463234e-16 + } + }, + { + "exponents": [ + 1, + 3, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00020528383893966728, + "original_integral": 0.0002052838389396674, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 5.281478454834189e-16, + "reduced_integral": 0.00020528383893966717, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 5.281478454834189e-16 + } + }, + { + "exponents": [ + 1, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.004001792513563149, + "original_integral": 0.004001792513563149, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.004001792513563154, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 1.300459834009907e-15 + } + }, + { + "exponents": [ + 1, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0018887893925728578, + "original_integral": 0.0018887893925728573, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.29607848656675e-16, + "reduced_integral": 0.0018887893925728589, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 5.740196216416875e-16 + } + }, + { + "exponents": [ + 1, + 4, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0009748525214156445, + "original_integral": 0.0009748525214156456, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 1.1121704551895363e-15, + "reduced_integral": 0.0009748525214156456, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 1.1121704551895363e-15 + } + }, + { + "exponents": [ + 1, + 4, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0005388161421626378, + "original_integral": 0.0005388161421626381, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 6.036579573138953e-16, + "reduced_integral": 0.0005388161421626382, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 8.04877276418527e-16 + } + }, + { + "exponents": [ + 1, + 4, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0003131576039811927, + "original_integral": 0.0003131576039811929, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 6.924322824686182e-16, + "reduced_integral": 0.0003131576039811927, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 1, + 4, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0001887723213854659, + "original_integral": 0.00018877232138546604, + "original_abs_error": 1.3552527156068805e-19, + "original_rel_error": 7.179297821101145e-16, + "reduced_integral": 0.00018877232138546574, + "reduced_abs_error": 1.6263032587282567e-19, + "reduced_rel_error": 8.615157385321374e-16 + } + }, + { + "exponents": [ + 1, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0023647179733550794, + "original_integral": 0.0023647179733550763, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 1.2837751127895581e-15, + "reduced_integral": 0.0023647179733550837, + "reduced_abs_error": 4.336808689942018e-18, + "reduced_rel_error": 1.833964446842226e-15 + } + }, + { + "exponents": [ + 1, + 5, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0011161133940256634, + "original_integral": 0.0011161133940256634, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0011161133940256645, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 9.714086205658194e-16 + } + }, + { + "exponents": [ + 1, + 5, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0005760546732367999, + "original_integral": 0.0005760546732368001, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 3.7642335800991564e-16, + "reduced_integral": 0.0005760546732368001, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 3.7642335800991564e-16 + } + }, + { + "exponents": [ + 1, + 5, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00031839437236873334, + "original_integral": 0.0003183943723687332, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 5.107826644765036e-16, + "reduced_integral": 0.0003183943723687332, + "reduced_abs_error": 1.6263032587282567e-19, + "reduced_rel_error": 5.107826644765036e-16 + } + }, + { + "exponents": [ + 1, + 5, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00018504942775450885, + "original_integral": 0.000185049427754509, + "original_abs_error": 1.3552527156068805e-19, + "original_rel_error": 7.323733621077674e-16, + "reduced_integral": 0.00018504942775450885, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 1, + 6, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0014356999488613598, + "original_integral": 0.00143569994886136, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.5103464666769333e-16, + "reduced_integral": 0.0014356999488613616, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 1.2082771733415467e-15 + } + }, + { + "exponents": [ + 1, + 6, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0006776300433208199, + "original_integral": 0.0006776300433208202, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 3.1999825957309135e-16, + "reduced_integral": 0.0006776300433208204, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 6.399965191461827e-16 + } + }, + { + "exponents": [ + 1, + 6, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00034974219937695494, + "original_integral": 0.00034974219937695505, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 3.100003872615162e-16, + "reduced_integral": 0.00034974219937695527, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 9.300011617845486e-16 + } + }, + { + "exponents": [ + 1, + 6, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0001933079501565135, + "original_integral": 0.00019330795015651357, + "original_abs_error": 5.421010862427522e-20, + "original_rel_error": 2.8043393238810677e-16, + "reduced_integral": 0.0001933079501565135, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 1, + 7, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0008895359798715205, + "original_integral": 0.0008895359798715203, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 2.4376803120253786e-16, + "reduced_integral": 0.0008895359798715221, + "reduced_abs_error": 1.6263032587282567e-18, + "reduced_rel_error": 1.828260234019034e-15 + } + }, + { + "exponents": [ + 1, + 7, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00041984838479225585, + "original_integral": 0.0004198483847922565, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 1.5494195691932589e-15, + "reduced_integral": 0.00041984838479225666, + "reduced_abs_error": 8.131516293641283e-19, + "reduced_rel_error": 1.9367744614915736e-15 + } + }, + { + "exponents": [ + 1, + 7, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0002166944912632596, + "original_integral": 0.00021669449126325978, + "original_abs_error": 1.8973538018496328e-19, + "original_rel_error": 8.755893104567019e-16, + "reduced_integral": 0.00021669449126325986, + "reduced_abs_error": 2.710505431213761e-19, + "reduced_rel_error": 1.2508418720810027e-15 + } + }, + { + "exponents": [ + 1, + 8, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.000559837333796513, + "original_integral": 0.0005598373337965132, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 3.8732757071881353e-16, + "reduced_integral": 0.0005598373337965141, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 1.9366378535940675e-15 + } + }, + { + "exponents": [ + 1, + 8, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0002642352930736066, + "original_integral": 0.0002642352930736066, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.00026423529307360714, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 2.0515847067096446e-15 + } + }, + { + "exponents": [ + 1, + 9, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00035673293403588576, + "original_integral": 0.000356732934035886, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 6.078508985528309e-16, + "reduced_integral": 0.0003567329340358865, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 2.127478144934908e-15 + } + }, + { + "exponents": [ + 2, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.02801645567318039, + "original_integral": 0.028016455673180386, + "original_abs_error": 3.469446951953614e-18, + "original_rel_error": 1.2383604094770806e-16, + "reduced_integral": 0.02801645567318036, + "reduced_abs_error": 2.7755575615628914e-17, + "reduced_rel_error": 9.906883275816645e-16 + } + }, + { + "exponents": [ + 2, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.013223370305591857, + "original_integral": 0.013223370305591873, + "original_abs_error": 1.5612511283791264e-17, + "original_rel_error": 1.1806756464491579e-15, + "reduced_integral": 0.013223370305591849, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 6.559309146939766e-16 + } + }, + { + "exponents": [ + 2, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.006824919673261954, + "original_integral": 0.006824919673261962, + "original_abs_error": 8.673617379884035e-18, + "original_rel_error": 1.270874646900936e-15, + "reduced_integral": 0.00682491967326195, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 5.083498587603743e-16 + } + }, + { + "exponents": [ + 2, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0037722391932440667, + "original_integral": 0.003772239193244071, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 1.149664288974324e-15, + "reduced_integral": 0.0037722391932440633, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 9.197314311794593e-16 + } + }, + { + "exponents": [ + 2, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0021924090519242294, + "original_integral": 0.0021924090519242294, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0021924090519242255, + "reduced_abs_error": 3.903127820947816e-18, + "reduced_rel_error": 1.7802917833796234e-15 + } + }, + { + "exponents": [ + 2, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0013215906013353603, + "original_integral": 0.0013215906013353607, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 3.281506909598195e-16, + "reduced_integral": 0.0013215906013353581, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 1.6407534547990973e-15 + } + }, + { + "exponents": [ + 2, + 0, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0008183386650231205, + "original_integral": 0.0008183386650231222, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.1198112103474863e-15, + "reduced_integral": 0.0008183386650231184, + "reduced_abs_error": 2.0599841277224584e-18, + "reduced_rel_error": 2.51727581228764e-15 + } + }, + { + "exponents": [ + 2, + 0, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0005170683359878634, + "original_integral": 0.0005170683359878644, + "original_abs_error": 9.75781955236954e-19, + "original_rel_error": 1.887143124656269e-15, + "reduced_integral": 0.0005170683359878616, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 3.3549211105000336e-15 + } + }, + { + "exponents": [ + 2, + 0, + 8 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00033185481427204993, + "original_integral": 0.00033185481427205074, + "original_abs_error": 8.131516293641283e-19, + "original_rel_error": 2.450323437819763e-15, + "reduced_integral": 0.00033185481427204874, + "reduced_abs_error": 1.1926223897340549e-18, + "reduced_rel_error": 3.593807708802319e-15 + } + }, + { + "exponents": [ + 2, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.012831563252858731, + "original_integral": 0.01283156325285873, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.351919046644866e-16, + "reduced_integral": 0.012831563252858726, + "reduced_abs_error": 5.204170427930421e-18, + "reduced_rel_error": 4.055757139934598e-16 + } + }, + { + "exponents": [ + 2, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00605631613332888, + "original_integral": 0.006056316133328876, + "original_abs_error": 4.336808689942018e-18, + "original_rel_error": 7.160803026902547e-16, + "reduced_integral": 0.006056316133328877, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 4.2964818161415286e-16 + } + }, + { + "exponents": [ + 2, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0031258196791457077, + "original_integral": 0.003125819679145709, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 4.1622445967138537e-16, + "reduced_integral": 0.003125819679145709, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 4.1622445967138537e-16 + } + }, + { + "exponents": [ + 2, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0017276891259075272, + "original_integral": 0.0017276891259075291, + "original_abs_error": 1.951563910473908e-18, + "original_rel_error": 1.1295804790394702e-15, + "reduced_integral": 0.0017276891259075276, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 2.5101788423099336e-16 + } + }, + { + "exponents": [ + 2, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.001004125423789278, + "original_integral": 0.0010041254237892787, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 6.478486532453526e-16, + "reduced_integral": 0.0010041254237892765, + "reduced_abs_error": 1.5178830414797062e-18, + "reduced_rel_error": 1.5116468575724894e-15 + } + }, + { + "exponents": [ + 2, + 1, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0006052897480408958, + "original_integral": 0.0006052897480408965, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 1.2538482985979403e-15, + "reduced_integral": 0.0006052897480408947, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 1.7912118551399146e-15 + } + }, + { + "exponents": [ + 2, + 1, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00037479988421790716, + "original_integral": 0.0003747998842179076, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.1570997944654152e-15, + "reduced_integral": 0.0003747998842179067, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 1.1570997944654152e-15 + } + }, + { + "exponents": [ + 2, + 1, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00023681778796987658, + "original_integral": 0.00023681778796987712, + "original_abs_error": 5.421010862427522e-19, + "original_rel_error": 2.289106282471095e-15, + "reduced_integral": 0.00023681778796987601, + "reduced_abs_error": 5.692061405548898e-19, + "reduced_rel_error": 2.40356159659465e-15 + } + }, + { + "exponents": [ + 2, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.006460544291672678, + "original_integral": 0.006460544291672669, + "original_abs_error": 8.673617379884035e-18, + "original_rel_error": 1.3425521114473126e-15, + "reduced_integral": 0.006460544291672676, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.685104222894625e-16 + } + }, + { + "exponents": [ + 2, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0030492854107254594, + "original_integral": 0.003049285410725459, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.4222377068043107e-16, + "reduced_integral": 0.0030492854107254594, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 2, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0015738142022877699, + "original_integral": 0.0015738142022877703, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.7556039865683194e-16, + "reduced_integral": 0.0015738142022877696, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 1.3778019932841597e-16 + } + }, + { + "exponents": [ + 2, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0008698715737289538, + "original_integral": 0.0008698715737289542, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 4.9855735270794565e-16, + "reduced_integral": 0.0008698715737289537, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 1.2463933817698641e-16 + } + }, + { + "exponents": [ + 2, + 2, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.000505565584406869, + "original_integral": 0.0005055655844068694, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 8.578132736289743e-16, + "reduced_integral": 0.0005055655844068686, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 6.433599552217307e-16 + } + }, + { + "exponents": [ + 2, + 2, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0003047564158359572, + "original_integral": 0.00030475641583595773, + "original_abs_error": 5.421010862427522e-19, + "original_rel_error": 1.7788012264015855e-15, + "reduced_integral": 0.00030475641583595686, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 1.0672807358409513e-15 + } + }, + { + "exponents": [ + 2, + 2, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00018870742440241, + "original_integral": 0.00018870742440241014, + "original_abs_error": 1.3552527156068805e-19, + "original_rel_error": 7.181766800636661e-16, + "reduced_integral": 0.00018870742440240973, + "reduced_abs_error": 2.710505431213761e-19, + "reduced_rel_error": 1.4363533601273322e-15 + } + }, + { + "exponents": [ + 2, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0034935838778848544, + "original_integral": 0.003493583877884855, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.241363837689703e-16, + "reduced_integral": 0.003493583877884859, + "reduced_abs_error": 4.7704895589362195e-18, + "reduced_rel_error": 1.3655002214586734e-15 + } + }, + { + "exponents": [ + 2, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.001648922113839706, + "original_integral": 0.0016489221138397056, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.630087045071678e-16, + "reduced_integral": 0.0016489221138397071, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 6.575217612679195e-16 + } + }, + { + "exponents": [ + 2, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0008510508829706097, + "original_integral": 0.0008510508829706093, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 5.09582773100981e-16, + "reduced_integral": 0.0008510508829706098, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 1.2739569327524526e-16 + } + }, + { + "exponents": [ + 2, + 3, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0004703890521618867, + "original_integral": 0.00047038905216188715, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 9.21962080114374e-16, + "reduced_integral": 0.00047038905216188667, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 1.1524526001429674e-16 + } + }, + { + "exponents": [ + 2, + 3, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00027338807616780254, + "original_integral": 0.0002733880761678029, + "original_abs_error": 3.7947076036992655e-19, + "original_rel_error": 1.3880296671644585e-15, + "reduced_integral": 0.00027338807616780265, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 3.9657990490413096e-16 + } + }, + { + "exponents": [ + 2, + 3, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00016479913347530295, + "original_integral": 0.00016479913347530308, + "original_abs_error": 1.3552527156068805e-19, + "original_rel_error": 8.22366408746913e-16, + "reduced_integral": 0.00016479913347530284, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 6.578931269975304e-16 + } + }, + { + "exponents": [ + 2, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.001989256841591169, + "original_integral": 0.0019892568415911713, + "original_abs_error": 2.168404344971009e-18, + "original_rel_error": 1.0900575026986173e-15, + "reduced_integral": 0.0019892568415911713, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 1.0900575026986173e-15 + } + }, + { + "exponents": [ + 2, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0009389010571552447, + "original_integral": 0.0009389010571552455, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 8.08329605080383e-16, + "reduced_integral": 0.0009389010571552457, + "reduced_abs_error": 9.75781955236954e-19, + "reduced_rel_error": 1.0392809208176352e-15 + } + }, + { + "exponents": [ + 2, + 4, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00048459085302296267, + "original_integral": 0.00048459085302296305, + "original_abs_error": 3.7947076036992655e-19, + "original_rel_error": 7.830745421683498e-16, + "reduced_integral": 0.0004845908530229631, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 8.949423339066854e-16 + } + }, + { + "exponents": [ + 2, + 4, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.000267840897178957, + "original_integral": 0.00026784089717895703, + "original_abs_error": 5.421010862427522e-20, + "original_rel_error": 2.0239668099698354e-16, + "reduced_integral": 0.00026784089717895687, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 4.047933619939671e-16 + } + }, + { + "exponents": [ + 2, + 4, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0001556679673182799, + "original_integral": 0.00015566796731827999, + "original_abs_error": 8.131516293641283e-20, + "original_rel_error": 5.223628491927002e-16, + "reduced_integral": 0.00015566796731827974, + "reduced_abs_error": 1.6263032587282567e-19, + "reduced_rel_error": 1.0447256983854004e-15 + } + }, + { + "exponents": [ + 2, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0011754810852853989, + "original_integral": 0.0011754810852853995, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 5.534085674661115e-16, + "reduced_integral": 0.0011754810852854008, + "reduced_abs_error": 1.951563910473908e-18, + "reduced_rel_error": 1.6602257023983345e-15 + } + }, + { + "exponents": [ + 2, + 5, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.000554810424961343, + "original_integral": 0.0005548104249613433, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 5.862554795510813e-16, + "reduced_integral": 0.0005548104249613437, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 1.1725109591021625e-15 + } + }, + { + "exponents": [ + 2, + 5, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00028635185257182527, + "original_integral": 0.0002863518525718254, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 3.78625862814544e-16, + "reduced_integral": 0.0002863518525718256, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 1.135877588443632e-15 + } + }, + { + "exponents": [ + 2, + 5, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0001582711201072955, + "original_integral": 0.00015827112010729564, + "original_abs_error": 1.3552527156068805e-19, + "original_rel_error": 8.562855400834496e-16, + "reduced_integral": 0.00015827112010729545, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 3.4251421603337983e-16 + } + }, + { + "exponents": [ + 2, + 6, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0007136741687793364, + "original_integral": 0.0007136741687793378, + "original_abs_error": 1.4094628242311558e-18, + "original_rel_error": 1.9749388248728292e-15, + "reduced_integral": 0.0007136741687793374, + "reduced_abs_error": 9.75781955236954e-19, + "reduced_rel_error": 1.3672653402965741e-15 + } + }, + { + "exponents": [ + 2, + 6, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00033684410053119816, + "original_integral": 0.0003368441005311984, + "original_abs_error": 2.710505431213761e-19, + "original_rel_error": 8.046765334287685e-16, + "reduced_integral": 0.00033684410053119853, + "reduced_abs_error": 3.7947076036992655e-19, + "reduced_rel_error": 1.1265471468002759e-15 + } + }, + { + "exponents": [ + 2, + 6, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00017385385687682315, + "original_integral": 0.0001738538568768233, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 9.35442726404687e-16, + "reduced_integral": 0.00017385385687682323, + "reduced_abs_error": 8.131516293641283e-20, + "reduced_rel_error": 4.677213632023435e-16 + } + }, + { + "exponents": [ + 2, + 7, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00044218072971138883, + "original_integral": 0.00044218072971138894, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 2.4519435145741485e-16, + "reduced_integral": 0.00044218072971138975, + "reduced_abs_error": 9.215718466126788e-19, + "reduced_rel_error": 2.0841519873880265e-15 + } + }, + { + "exponents": [ + 2, + 7, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00020870304221129067, + "original_integral": 0.00020870304221129056, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 5.194951453500422e-16, + "reduced_integral": 0.0002087030422112909, + "reduced_abs_error": 2.439454888092385e-19, + "reduced_rel_error": 1.168864077037595e-15 + } + }, + { + "exponents": [ + 2, + 8, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0002782903518006941, + "original_integral": 0.0002782903518006942, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 3.8959387757071367e-16, + "reduced_integral": 0.00027829035180069474, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 2.337563265424282e-15 + } + }, + { + "exponents": [ + 3, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.01498059689723164, + "original_integral": 0.014980596897231625, + "original_abs_error": 1.5612511283791264e-17, + "original_rel_error": 1.0421821901286456e-15, + "reduced_integral": 0.014980596897231616, + "reduced_abs_error": 2.42861286636753e-17, + "reduced_rel_error": 1.621172295755671e-15 + } + }, + { + "exponents": [ + 3, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.007070629578620323, + "original_integral": 0.007070629578620324, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.2267107594082873e-16, + "reduced_integral": 0.007070629578620317, + "reduced_abs_error": 6.071532165918825e-18, + "reduced_rel_error": 8.586975315858012e-16 + } + }, + { + "exponents": [ + 3, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0036493327947616466, + "original_integral": 0.0036493327947616453, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 3.5651519884680235e-16, + "reduced_integral": 0.003649332794761641, + "reduced_abs_error": 5.637851296924623e-18, + "reduced_rel_error": 1.5448991950028102e-15 + } + }, + { + "exponents": [ + 3, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.002017042962647982, + "original_integral": 0.0020170429626479823, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.150082457464683e-16, + "reduced_integral": 0.0020170429626479784, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 1.7200659659717464e-15 + } + }, + { + "exponents": [ + 3, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0011722966182392302, + "original_integral": 0.0011722966182392306, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 3.699412437490293e-16, + "reduced_integral": 0.0011722966182392278, + "reduced_abs_error": 2.3852447794681098e-18, + "reduced_rel_error": 2.034676840619661e-15 + } + }, + { + "exponents": [ + 3, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0007066638368795324, + "original_integral": 0.0007066638368795335, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 1.534254501083706e-15, + "reduced_integral": 0.0007066638368795303, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 3.068509002167412e-15 + } + }, + { + "exponents": [ + 3, + 0, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00043757146903723206, + "original_integral": 0.00043757146903723303, + "original_abs_error": 9.75781955236954e-19, + "original_rel_error": 2.229994467838411e-15, + "reduced_integral": 0.00043757146903723054, + "reduced_abs_error": 1.5178830414797062e-18, + "reduced_rel_error": 3.468880283304195e-15 + } + }, + { + "exponents": [ + 3, + 0, + 7 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0002764800944172107, + "original_integral": 0.00027648009441721104, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 1.1764342472150286e-15, + "reduced_integral": 0.00027648009441720963, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 3.9214474907167625e-15 + } + }, + { + "exponents": [ + 3, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.006861127577833471, + "original_integral": 0.006861127577833473, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 2.528335840279738e-16, + "reduced_integral": 0.006861127577833469, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.528335840279738e-16 + } + }, + { + "exponents": [ + 3, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.003238355048688473, + "original_integral": 0.0032383550486884717, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 4.0176033431217643e-16, + "reduced_integral": 0.0032383550486884695, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 1.0713608914991372e-15 + } + }, + { + "exponents": [ + 3, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0016713978789095016, + "original_integral": 0.0016713978789095034, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 1.0378878050920001e-15, + "reduced_integral": 0.0016713978789094988, + "reduced_abs_error": 2.8189256484623115e-18, + "reduced_rel_error": 1.6865676832745e-15 + } + }, + { + "exponents": [ + 3, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0009238075886853635, + "original_integral": 0.0009238075886853645, + "original_abs_error": 9.75781955236954e-19, + "original_rel_error": 1.0562610300977862e-15, + "reduced_integral": 0.0009238075886853626, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 9.388986934202543e-16 + } + }, + { + "exponents": [ + 3, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0005369129622791243, + "original_integral": 0.0005369129622791253, + "original_abs_error": 9.75781955236954e-19, + "original_rel_error": 1.8173931787656776e-15, + "reduced_integral": 0.0005369129622791234, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 1.6154606033472689e-15 + } + }, + { + "exponents": [ + 3, + 1, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0003236527070805676, + "original_integral": 0.0003236527070805681, + "original_abs_error": 4.87890977618477e-19, + "original_rel_error": 1.5074521761902803e-15, + "reduced_integral": 0.0003236527070805673, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 1.004968117460187e-15 + } + }, + { + "exponents": [ + 3, + 1, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00020040814755780927, + "original_integral": 0.00020040814755780946, + "original_abs_error": 1.8973538018496328e-19, + "original_rel_error": 9.467448429472293e-16, + "reduced_integral": 0.00020040814755780894, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 1.6229911593381072e-15 + } + }, + { + "exponents": [ + 3, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.003454498702450346, + "original_integral": 0.00345449870245035, + "original_abs_error": 3.903127820947816e-18, + "original_rel_error": 1.1298680813454201e-15, + "reduced_integral": 0.0034544987024503464, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 1.255408979272689e-16 + } + }, + { + "exponents": [ + 3, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0016304744645632033, + "original_integral": 0.0016304744645632041, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 5.319689187654747e-16, + "reduced_integral": 0.0016304744645632033, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 3, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0008415295792815345, + "original_integral": 0.0008415295792815349, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 5.1534833673281195e-16, + "reduced_integral": 0.0008415295792815341, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 3.8651125254960896e-16 + } + }, + { + "exponents": [ + 3, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00046512647960921324, + "original_integral": 0.0004651264796092134, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 3.4964753245066437e-16, + "reduced_integral": 0.00046512647960921314, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 2.3309835496710956e-16 + } + }, + { + "exponents": [ + 3, + 2, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00027032949183371447, + "original_integral": 0.00027032949183371485, + "original_abs_error": 3.7947076036992655e-19, + "original_rel_error": 1.4037342274269774e-15, + "reduced_integral": 0.0002703294918337142, + "reduced_abs_error": 2.710505431213761e-19, + "reduced_rel_error": 1.0026673053049838e-15 + } + }, + { + "exponents": [ + 3, + 2, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00016295540987556025, + "original_integral": 0.0001629554098755604, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 9.98005073885041e-16, + "reduced_integral": 0.00016295540987556012, + "reduced_abs_error": 1.3552527156068805e-19, + "reduced_rel_error": 8.316708949042009e-16 + } + }, + { + "exponents": [ + 3, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0018680439957064418, + "original_integral": 0.001868043995706441, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 4.643154764994664e-16, + "reduced_integral": 0.0018680439957064435, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 9.286309529989327e-16 + } + }, + { + "exponents": [ + 3, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0008816903105274064, + "original_integral": 0.000881690310527407, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 7.378115600501225e-16, + "reduced_integral": 0.0008816903105274069, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 6.148429667084354e-16 + } + }, + { + "exponents": [ + 3, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00045506292321695677, + "original_integral": 0.00045506292321695693, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 3.5737986457598014e-16, + "reduced_integral": 0.0004550629232169568, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 1.1912662152532672e-16 + } + }, + { + "exponents": [ + 3, + 3, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00025152035137884234, + "original_integral": 0.00025152035137884256, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 8.621188436974381e-16, + "reduced_integral": 0.00025152035137884223, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 4.3105942184871905e-16 + } + }, + { + "exponents": [ + 3, + 3, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0001461825369116932, + "original_integral": 0.00014618253691169324, + "original_abs_error": 2.710505431213761e-20, + "original_rel_error": 1.8541923600977995e-16, + "reduced_integral": 0.00014618253691169307, + "reduced_abs_error": 1.3552527156068805e-19, + "reduced_rel_error": 9.270961800488996e-16 + } + }, + { + "exponents": [ + 3, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0010636696952878546, + "original_integral": 0.0010636696952878544, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 2.0386068669411387e-16, + "reduced_integral": 0.001063669695287856, + "reduced_abs_error": 1.5178830414797062e-18, + "reduced_rel_error": 1.427024806858797e-15 + } + }, + { + "exponents": [ + 3, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0005020370323677951, + "original_integral": 0.0005020370323677958, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 1.511724179310837e-15, + "reduced_integral": 0.0005020370323677955, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 8.638423881776212e-16 + } + }, + { + "exponents": [ + 3, + 4, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0002591141546920216, + "original_integral": 0.00025911415469202147, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 4.1842645523328034e-16, + "reduced_integral": 0.0002591141546920216, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 3, + 4, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00014321642109325872, + "original_integral": 0.00014321642109325897, + "original_abs_error": 2.439454888092385e-19, + "original_rel_error": 1.7033346242494615e-15, + "reduced_integral": 0.0001432164210932587, + "reduced_abs_error": 2.710505431213761e-20, + "reduced_rel_error": 1.892594026943846e-16 + } + }, + { + "exponents": [ + 3, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0006285380457970661, + "original_integral": 0.0006285380457970658, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 5.174876110055988e-16, + "reduced_integral": 0.0006285380457970667, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 8.624793516759979e-16 + } + }, + { + "exponents": [ + 3, + 5, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0002966610561907728, + "original_integral": 0.00029666105619077283, + "original_abs_error": 5.421010862427522e-20, + "original_rel_error": 1.8273415904450404e-16, + "reduced_integral": 0.000296661056190773, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 7.309366361780162e-16 + } + }, + { + "exponents": [ + 3, + 5, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00015311435979607107, + "original_integral": 0.00015311435979607112, + "original_abs_error": 5.421010862427522e-20, + "original_rel_error": 3.5404980105377587e-16, + "reduced_integral": 0.00015311435979607112, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 3.5404980105377587e-16 + } + }, + { + "exponents": [ + 3, + 6, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00038160662302064976, + "original_integral": 0.00038160662302064976, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.00038160662302065036, + "reduced_abs_error": 5.963111948670274e-19, + "reduced_rel_error": 1.5626332429632895e-15 + } + }, + { + "exponents": [ + 3, + 6, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.000180112921710472, + "original_integral": 0.00018011292171047182, + "original_abs_error": 1.8973538018496328e-19, + "original_rel_error": 1.053424587104079e-15, + "reduced_integral": 0.00018011292171047212, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 6.019569069166165e-16 + } + }, + { + "exponents": [ + 3, + 7, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00023643716195946964, + "original_integral": 0.00023643716195946953, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 4.585582754843588e-16, + "reduced_integral": 0.00023643716195947007, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 1.8342331019374353e-15 + } + }, + { + "exponents": [ + 4, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.008440467836218837, + "original_integral": 0.008440467836218851, + "original_abs_error": 1.3877787807814457e-17, + "original_rel_error": 1.6441965157741104e-15, + "reduced_integral": 0.008440467836218836, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 2.055245644717638e-16 + } + }, + { + "exponents": [ + 4, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.003983781283854647, + "original_integral": 0.003983781283854649, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 4.3544646464584893e-16, + "reduced_integral": 0.003983781283854638, + "reduced_abs_error": 8.673617379884035e-18, + "reduced_rel_error": 2.1772323232292448e-15 + } + }, + { + "exponents": [ + 4, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0020561314271487006, + "original_integral": 0.0020561314271487037, + "original_abs_error": 3.0357660829594124e-18, + "original_rel_error": 1.4764455437409468e-15, + "reduced_integral": 0.002056131427148698, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 1.2655247517779545e-15 + } + }, + { + "exponents": [ + 4, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0011364558012803866, + "original_integral": 0.0011364558012803869, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.9080410716615452e-16, + "reduced_integral": 0.0011364558012803832, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 3.0528657146584724e-15 + } + }, + { + "exponents": [ + 4, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0006605031807901359, + "original_integral": 0.0006605031807901366, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 9.848874652096417e-16, + "reduced_integral": 0.0006605031807901343, + "reduced_abs_error": 1.6263032587282567e-18, + "reduced_rel_error": 2.4622186630241044e-15 + } + }, + { + "exponents": [ + 4, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00039815325297905344, + "original_integral": 0.00039815325297905344, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0003981532529790521, + "reduced_abs_error": 1.3552527156068805e-18, + "reduced_rel_error": 3.4038468993198943e-15 + } + }, + { + "exponents": [ + 4, + 0, + 6 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00024653943603130367, + "original_integral": 0.00024653943603130394, + "original_abs_error": 2.710505431213761e-19, + "original_rel_error": 1.0994206342183738e-15, + "reduced_integral": 0.00024653943603130274, + "reduced_abs_error": 9.215718466126788e-19, + "reduced_rel_error": 3.738030156342471e-15 + } + }, + { + "exponents": [ + 4, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.003865742269027963, + "original_integral": 0.0038657422690279637, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 2.243713309440313e-16, + "reduced_integral": 0.003865742269027962, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 2.243713309440313e-16 + } + }, + { + "exponents": [ + 4, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.001824575603910886, + "original_integral": 0.0018245756039108856, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 2.3768862636584016e-16, + "reduced_integral": 0.001824575603910885, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 5.942215659146004e-16 + } + }, + { + "exponents": [ + 4, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0009417101424755005, + "original_integral": 0.0009417101424755011, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 6.90787190399435e-16, + "reduced_integral": 0.0009417101424754999, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 5.756559919995292e-16 + } + }, + { + "exponents": [ + 4, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00052049783414135, + "original_integral": 0.0005204978341413503, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 4.1660199192724053e-16, + "reduced_integral": 0.0005204978341413494, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 1.2498059757817215e-15 + } + }, + { + "exponents": [ + 4, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0003025110828396557, + "original_integral": 0.000302511082839656, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 1.0752024312380446e-15, + "reduced_integral": 0.0003025110828396553, + "reduced_abs_error": 3.7947076036992655e-19, + "reduced_rel_error": 1.2544028364443855e-15 + } + }, + { + "exponents": [ + 4, + 1, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0001823545672418109, + "original_integral": 0.0001823545672418111, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.1891143598808801e-15, + "reduced_integral": 0.00018235456724181043, + "reduced_abs_error": 4.607859233063394e-19, + "reduced_rel_error": 2.5268680147468706e-15 + } + }, + { + "exponents": [ + 4, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.001946356703161814, + "original_integral": 0.0019463567031618122, + "original_abs_error": 1.734723475976807e-18, + "original_rel_error": 8.91266987782243e-16, + "reduced_integral": 0.001946356703161813, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 4.456334938911215e-16 + } + }, + { + "exponents": [ + 4, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0009186527993731023, + "original_integral": 0.0009186527993731027, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 4.720835437394301e-16, + "reduced_integral": 0.0009186527993731024, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 1.1802088593485752e-16 + } + }, + { + "exponents": [ + 4, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0004741402092239166, + "original_integral": 0.0004741402092239167, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 2.2866699583655876e-16, + "reduced_integral": 0.0004741402092239165, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 1.1433349791827938e-16 + } + }, + { + "exponents": [ + 4, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0002620646639013932, + "original_integral": 0.00026206466390139334, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 6.205732716945709e-16, + "reduced_integral": 0.00026206466390139334, + "reduced_abs_error": 1.6263032587282567e-19, + "reduced_rel_error": 6.205732716945709e-16 + } + }, + { + "exponents": [ + 4, + 2, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0001523108455995837, + "original_integral": 0.0001523108455995838, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 7.118351737970181e-16, + "reduced_integral": 0.00015231084559958358, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 7.118351737970181e-16 + } + }, + { + "exponents": [ + 4, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0010525058093857175, + "original_integral": 0.0010525058093857169, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 6.180690858808387e-16, + "reduced_integral": 0.0010525058093857175, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 4, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0004967678363261753, + "original_integral": 0.000496767836326175, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 4.365025644589529e-16, + "reduced_integral": 0.0004967678363261757, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 8.730051289179058e-16 + } + }, + { + "exponents": [ + 4, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00025639458782702047, + "original_integral": 0.0002563945878270209, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 1.6914587498500144e-15, + "reduced_integral": 0.0002563945878270206, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 4.228646874625036e-16 + } + }, + { + "exponents": [ + 4, + 3, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00014171327421271798, + "original_integral": 0.00014171327421271795, + "original_abs_error": 2.710505431213761e-20, + "original_rel_error": 1.9126686940739023e-16, + "reduced_integral": 0.00014171327421271787, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 7.650674776295609e-16 + } + }, + { + "exponents": [ + 4, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0005992998752337373, + "original_integral": 0.0005992998752337378, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 7.23645852295662e-16, + "reduced_integral": 0.0005992998752337371, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 3.61822926147831e-16 + } + }, + { + "exponents": [ + 4, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00028286105375909253, + "original_integral": 0.0002828610537590928, + "original_abs_error": 2.710505431213761e-19, + "original_rel_error": 9.582462467675906e-16, + "reduced_integral": 0.0002828610537590925, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 1.9164924935351814e-16 + } + }, + { + "exponents": [ + 4, + 4, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00014599182553207872, + "original_integral": 0.00014599182553207883, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 7.426458081019565e-16, + "reduced_integral": 0.00014599182553207872, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 4, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00035413509860680897, + "original_integral": 0.0003541350986068095, + "original_abs_error": 5.421010862427522e-19, + "original_rel_error": 1.530774804235485e-15, + "reduced_integral": 0.00035413509860680897, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 4, + 5, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00016714675124191147, + "original_integral": 0.0001671467512419114, + "original_abs_error": 5.421010862427522e-20, + "original_rel_error": 3.2432642705580883e-16, + "reduced_integral": 0.00016714675124191168, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 1.2973057082232353e-15 + } + }, + { + "exponents": [ + 4, + 6, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0002150073491590379, + "original_integral": 0.0002150073491590377, + "original_abs_error": 1.8973538018496328e-19, + "original_rel_error": 8.824599760290924e-16, + "reduced_integral": 0.00021500734915903805, + "reduced_abs_error": 1.6263032587282567e-19, + "reduced_rel_error": 7.563942651677934e-16 + } + }, + { + "exponents": [ + 5, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0049366547789406945, + "original_integral": 0.004936654778940692, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 5.270948305045477e-16, + "reduced_integral": 0.004936654778940693, + "reduced_abs_error": 1.734723475976807e-18, + "reduced_rel_error": 3.513965536696985e-16 + } + }, + { + "exponents": [ + 5, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00233003114220808, + "original_integral": 0.002330031142208081, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 3.7225328120139994e-16, + "reduced_integral": 0.0023300311422080767, + "reduced_abs_error": 3.469446951953614e-18, + "reduced_rel_error": 1.4890131248055998e-15 + } + }, + { + "exponents": [ + 5, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0012025886755242907, + "original_integral": 0.0012025886755242922, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 1.2621797231027118e-15, + "reduced_integral": 0.0012025886755242874, + "reduced_abs_error": 3.2526065174565133e-18, + "reduced_rel_error": 2.704670835220097e-15 + } + }, + { + "exponents": [ + 5, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0006646894545787399, + "original_integral": 0.0006646894545787413, + "original_abs_error": 1.4094628242311558e-18, + "original_rel_error": 2.120483203881173e-15, + "reduced_integral": 0.0006646894545787386, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 1.957369111274929e-15 + } + }, + { + "exponents": [ + 5, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0003863146270117028, + "original_integral": 0.00038631462701170294, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 4.2097894954388835e-16, + "reduced_integral": 0.00038631462701170147, + "reduced_abs_error": 1.3010426069826053e-18, + "reduced_rel_error": 3.3678315963511068e-15 + } + }, + { + "exponents": [ + 5, + 0, + 5 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00023287158925426967, + "original_integral": 0.00023287158925426978, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 4.655794105057947e-16, + "reduced_integral": 0.00023287158925426864, + "reduced_abs_error": 1.0299920638612292e-18, + "reduced_rel_error": 4.42300439980505e-15 + } + }, + { + "exponents": [ + 5, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0022609925678123447, + "original_integral": 0.0022609925678123474, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 1.1508596936622816e-15, + "reduced_integral": 0.0022609925678123456, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 3.836198978874272e-16 + } + }, + { + "exponents": [ + 5, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0010671564715801782, + "original_integral": 0.0010671564715801784, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 2.0319460198373458e-16, + "reduced_integral": 0.0010671564715801771, + "reduced_abs_error": 1.0842021724855044e-18, + "reduced_rel_error": 1.015973009918673e-15 + } + }, + { + "exponents": [ + 5, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0005507867532270834, + "original_integral": 0.0005507867532270835, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 1.968460871894825e-16, + "reduced_integral": 0.0005507867532270833, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 1.968460871894825e-16 + } + }, + { + "exponents": [ + 5, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.000304428400202668, + "original_integral": 0.00030442840020266835, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 1.0684307099111468e-15, + "reduced_integral": 0.00030442840020266775, + "reduced_abs_error": 2.710505431213761e-19, + "reduced_rel_error": 8.903589249259557e-16 + } + }, + { + "exponents": [ + 5, + 1, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00017693246532788406, + "original_integral": 0.00017693246532788411, + "original_abs_error": 5.421010862427522e-20, + "original_rel_error": 3.063887033044798e-16, + "reduced_integral": 0.00017693246532788373, + "reduced_abs_error": 3.2526065174565133e-19, + "reduced_rel_error": 1.8383322198268786e-15 + } + }, + { + "exponents": [ + 5, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0011383837136320907, + "original_integral": 0.001138383713632092, + "original_abs_error": 1.3010426069826053e-18, + "original_rel_error": 1.1428858225945102e-15, + "reduced_integral": 0.0011383837136320915, + "reduced_abs_error": 8.673617379884035e-19, + "reduced_rel_error": 7.619238817296735e-16 + } + }, + { + "exponents": [ + 5, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0005373009909180692, + "original_integral": 0.0005373009909180693, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 2.0178674352209226e-16, + "reduced_integral": 0.000537300990918069, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 4.035734870441845e-16 + } + }, + { + "exponents": [ + 5, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.000277314785764501, + "original_integral": 0.0002773147857645012, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 5.864466455493407e-16, + "reduced_integral": 0.00027731478576450096, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 1.9548221518311354e-16 + } + }, + { + "exponents": [ + 5, + 2, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00015327619280637673, + "original_integral": 0.00015327619280637694, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.4147039440823045e-15, + "reduced_integral": 0.00015327619280637665, + "reduced_abs_error": 8.131516293641283e-20, + "reduced_rel_error": 5.305139790308642e-16 + } + }, + { + "exponents": [ + 5, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0006155888434845912, + "original_integral": 0.000615588843484592, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 1.2328708175473134e-15, + "reduced_integral": 0.0006155888434845914, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 3.522488050135181e-16 + } + }, + { + "exponents": [ + 5, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0002905492160873225, + "original_integral": 0.0002905492160873226, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 3.731561169174365e-16, + "reduced_integral": 0.00029054921608732254, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 1.8657805845871826e-16 + } + }, + { + "exponents": [ + 5, + 3, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00014995988277562267, + "original_integral": 0.0001499598827756227, + "original_abs_error": 2.710505431213761e-20, + "original_rel_error": 1.80748702989409e-16, + "reduced_integral": 0.0001499598827756226, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 3.61497405978818e-16 + } + }, + { + "exponents": [ + 5, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00035051808152100703, + "original_integral": 0.00035051808152100655, + "original_abs_error": 4.87890977618477e-19, + "original_rel_error": 1.3919138650461802e-15, + "reduced_integral": 0.0003505180815210072, + "reduced_abs_error": 1.6263032587282567e-19, + "reduced_rel_error": 4.639712883487268e-16 + } + }, + { + "exponents": [ + 5, + 4, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00016543957040200975, + "original_integral": 0.00016543957040200975, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0001654395704020098, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 3.276731708898144e-16 + } + }, + { + "exponents": [ + 5, + 5, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00020712628267192297, + "original_integral": 0.00020712628267192284, + "original_abs_error": 1.3552527156068805e-19, + "original_rel_error": 6.543122862652485e-16, + "reduced_integral": 0.00020712628267192292, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 2.617249145060994e-16 + } + }, + { + "exponents": [ + 6, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0029669160778858826, + "original_integral": 0.00296691607788588, + "original_abs_error": 2.6020852139652106e-18, + "original_rel_error": 8.770336422253516e-16, + "reduced_integral": 0.0029669160778858796, + "reduced_abs_error": 3.0357660829594124e-18, + "reduced_rel_error": 1.0232059159295768e-15 + } + }, + { + "exponents": [ + 6, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0014003423709679262, + "original_integral": 0.0014003423709679273, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 7.742407820853813e-16, + "reduced_integral": 0.001400342370967924, + "reduced_abs_error": 2.168404344971009e-18, + "reduced_rel_error": 1.5484815641707626e-15 + } + }, + { + "exponents": [ + 6, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0007227525189156776, + "original_integral": 0.0007227525189156776, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0007227525189156762, + "reduced_abs_error": 1.4094628242311558e-18, + "reduced_rel_error": 1.9501320124705033e-15 + } + }, + { + "exponents": [ + 6, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00039947655201728096, + "original_integral": 0.00039947655201728156, + "original_abs_error": 5.963111948670274e-19, + "original_rel_error": 1.4927314052746495e-15, + "reduced_integral": 0.00039947655201727934, + "reduced_abs_error": 1.6263032587282567e-18, + "reduced_rel_error": 4.071085650749044e-15 + } + }, + { + "exponents": [ + 6, + 0, + 4 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00023217403876263175, + "original_integral": 0.00023217403876263216, + "original_abs_error": 4.0657581468206416e-19, + "original_rel_error": 1.7511682910324694e-15, + "reduced_integral": 0.00023217403876263083, + "reduced_abs_error": 9.215718466126788e-19, + "reduced_rel_error": 3.969314793006931e-15 + } + }, + { + "exponents": [ + 6, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0013588503757725524, + "original_integral": 0.0013588503757725515, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 6.383055511135867e-16, + "reduced_integral": 0.0013588503757725522, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 1.5957638777839669e-16 + } + }, + { + "exponents": [ + 6, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0006413581331750713, + "original_integral": 0.0006413581331750718, + "original_abs_error": 5.421010862427522e-19, + "original_rel_error": 8.452392792760844e-16, + "reduced_integral": 0.0006413581331750715, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 3.3809571171043375e-16 + } + }, + { + "exponents": [ + 6, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00033102133870228825, + "original_integral": 0.00033102133870228847, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 6.550648225494653e-16, + "reduced_integral": 0.0003310213387022876, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 1.9651944676483957e-15 + } + }, + { + "exponents": [ + 6, + 1, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0001829606394555676, + "original_integral": 0.000182960639455568, + "original_abs_error": 4.0657581468206416e-19, + "original_rel_error": 2.2222037258500184e-15, + "reduced_integral": 0.00018296063945556743, + "reduced_abs_error": 1.6263032587282567e-19, + "reduced_rel_error": 8.888814903400072e-16 + } + }, + { + "exponents": [ + 6, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0006841655116712915, + "original_integral": 0.0006841655116712926, + "original_abs_error": 1.0842021724855044e-18, + "original_rel_error": 1.5847074340783363e-15, + "reduced_integral": 0.0006841655116712921, + "reduced_abs_error": 5.421010862427522e-19, + "reduced_rel_error": 7.923537170391681e-16 + } + }, + { + "exponents": [ + 6, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00032291643228107234, + "original_integral": 0.00032291643228107283, + "original_abs_error": 4.87890977618477e-19, + "original_rel_error": 1.510889285416754e-15, + "reduced_integral": 0.00032291643228107224, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 3.3575317453705644e-16 + } + }, + { + "exponents": [ + 6, + 2, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00016666543101819372, + "original_integral": 0.0001666654310181938, + "original_abs_error": 8.131516293641283e-20, + "original_rel_error": 4.878945948157433e-16, + "reduced_integral": 0.00016666543101819363, + "reduced_abs_error": 8.131516293641283e-20, + "reduced_rel_error": 4.878945948157433e-16 + } + }, + { + "exponents": [ + 6, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00036996721846803264, + "original_integral": 0.0003699672184680329, + "original_abs_error": 2.710505431213761e-19, + "original_rel_error": 7.326339459040382e-16, + "reduced_integral": 0.0003699672184680327, + "reduced_abs_error": 5.421010862427522e-20, + "reduced_rel_error": 1.4652678918080764e-16 + } + }, + { + "exponents": [ + 6, + 3, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00017461928760017348, + "original_integral": 0.0001746192876001736, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 6.208948549647086e-16, + "reduced_integral": 0.0001746192876001735, + "reduced_abs_error": 2.710505431213761e-20, + "reduced_rel_error": 1.5522371374117714e-16 + } + }, + { + "exponents": [ + 6, + 4, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00021066041240938121, + "original_integral": 0.00021066041240938105, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 7.720023141167237e-16, + "reduced_integral": 0.00021066041240938149, + "reduced_abs_error": 2.710505431213761e-19, + "reduced_rel_error": 1.2866705235278727e-15 + } + }, + { + "exponents": [ + 7, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0018197428512164628, + "original_integral": 0.001819742851216463, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 1.1915993204872175e-16, + "reduced_integral": 0.0018197428512164602, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 1.4299191845846611e-15 + } + }, + { + "exponents": [ + 7, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0008588928543742958, + "original_integral": 0.0008588928543742973, + "original_abs_error": 1.5178830414797062e-18, + "original_rel_error": 1.7672554076439317e-15, + "reduced_integral": 0.0008588928543742938, + "reduced_abs_error": 1.951563910473908e-18, + "reduced_rel_error": 2.2721855241136264e-15 + } + }, + { + "exponents": [ + 7, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00044329657292924705, + "original_integral": 0.0004432965729292479, + "original_abs_error": 8.673617379884035e-19, + "original_rel_error": 1.9566172872869018e-15, + "reduced_integral": 0.0004432965729292461, + "reduced_abs_error": 9.215718466126788e-19, + "reduced_rel_error": 2.078905867742333e-15 + } + }, + { + "exponents": [ + 7, + 0, + 3 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00024501690667302006, + "original_integral": 0.0002450169066730201, + "original_abs_error": 5.421010862427522e-20, + "original_rel_error": 2.21250481692758e-16, + "reduced_integral": 0.0002450169066730193, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 3.097506743698612e-15 + } + }, + { + "exponents": [ + 7, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0008334439506448411, + "original_integral": 0.0008334439506448407, + "original_abs_error": 3.2526065174565133e-19, + "original_rel_error": 3.9026097855050124e-16, + "reduced_integral": 0.000833443950644841, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 1.3008699285016708e-16 + } + }, + { + "exponents": [ + 7, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0003933737413787957, + "original_integral": 0.00039337374137879584, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 4.1342445813184633e-16, + "reduced_integral": 0.0003933737413787955, + "reduced_abs_error": 1.6263032587282567e-19, + "reduced_rel_error": 4.1342445813184633e-16 + } + }, + { + "exponents": [ + 7, + 1, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00020303025056671713, + "original_integral": 0.00020303025056671743, + "original_abs_error": 2.981555974335137e-19, + "original_rel_error": 1.4685279489202904e-15, + "reduced_integral": 0.00020303025056671688, + "reduced_abs_error": 2.439454888092385e-19, + "reduced_rel_error": 1.2015228672984193e-15 + } + }, + { + "exponents": [ + 7, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00041962942875008186, + "original_integral": 0.00041962942875008186, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.00041962942875008175, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 2.583713386630043e-16 + } + }, + { + "exponents": [ + 7, + 2, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00019805914753157652, + "original_integral": 0.00019805914753157669, + "original_abs_error": 1.6263032587282567e-19, + "original_rel_error": 8.211199931924252e-16, + "reduced_integral": 0.00019805914753157644, + "reduced_abs_error": 8.131516293641283e-20, + "reduced_rel_error": 4.105599965962126e-16 + } + }, + { + "exponents": [ + 7, + 3, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0002269175073773478, + "original_integral": 0.00022691750737734767, + "original_abs_error": 1.3552527156068805e-19, + "original_rel_error": 5.972446688977554e-16, + "reduced_integral": 0.000226917507377348, + "reduced_abs_error": 1.8973538018496328e-19, + "reduced_rel_error": 8.361425364568575e-16 + } + }, + { + "exponents": [ + 8, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0011337546528181915, + "original_integral": 0.001133754652818191, + "original_abs_error": 4.336808689942018e-19, + "original_rel_error": 3.8251738849864434e-16, + "reduced_integral": 0.0011337546528181889, + "reduced_abs_error": 2.6020852139652106e-18, + "reduced_rel_error": 2.295104330991866e-15 + } + }, + { + "exponents": [ + 8, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0005351161397711807, + "original_integral": 0.0005351161397711814, + "original_abs_error": 6.505213034913027e-19, + "original_rel_error": 1.2156637693071078e-15, + "reduced_integral": 0.00053511613977118, + "reduced_abs_error": 7.589415207398531e-19, + "reduced_rel_error": 1.418274397524959e-15 + } + }, + { + "exponents": [ + 8, + 0, + 2 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0002761871281983175, + "original_integral": 0.0002761871281983176, + "original_abs_error": 5.421010862427522e-20, + "original_rel_error": 1.9628035882015973e-16, + "reduced_integral": 0.0002761871281983167, + "reduced_abs_error": 8.131516293641283e-19, + "reduced_rel_error": 2.944205382302396e-15 + } + }, + { + "exponents": [ + 8, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0005192607055854634, + "original_integral": 0.0005192607055854634, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0005192607055854637, + "reduced_abs_error": 2.168404344971009e-19, + "reduced_rel_error": 4.1759453808971457e-16 + } + }, + { + "exponents": [ + 8, + 1, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00024508369920869566, + "original_integral": 0.00024508369920869577, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 4.423803688234181e-16, + "reduced_integral": 0.00024508369920869555, + "reduced_abs_error": 1.0842021724855044e-19, + "reduced_rel_error": 4.423803688234181e-16 + } + }, + { + "exponents": [ + 8, + 2, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00026144178392392697, + "original_integral": 0.0002614417839239272, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 8.294023672979375e-16, + "reduced_integral": 0.00026144178392392735, + "reduced_abs_error": 3.7947076036992655e-19, + "reduced_rel_error": 1.4514541427713906e-15 + } + }, + { + "exponents": [ + 9, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0007151762907816549, + "original_integral": 0.0007151762907816549, + "original_abs_error": 0.0, + "original_rel_error": 0.0, + "reduced_integral": 0.0007151762907816545, + "reduced_abs_error": 4.336808689942018e-19, + "reduced_rel_error": 6.063971563154149e-16 + } + }, + { + "exponents": [ + 9, + 0, + 1 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.00033755308084307424, + "original_integral": 0.000337553080843075, + "original_abs_error": 7.589415207398531e-19, + "original_rel_error": 2.248362002338468e-15, + "reduced_integral": 0.0003375530808430736, + "reduced_abs_error": 6.505213034913027e-19, + "reduced_rel_error": 1.9271674305758296e-15 + } + }, + { + "exponents": [ + 9, + 1, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0003275514190360093, + "original_integral": 0.0003275514190360092, + "original_abs_error": 1.0842021724855044e-19, + "original_rel_error": 3.3100212958207724e-16, + "reduced_integral": 0.0003275514190360093, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + }, + { + "exponents": [ + 10, + 0, + 0 + ], + "result": { + "original_strength": 10, + "reduced_strength": 10, + "original_points": 486, + "reduced_points": 286, + "exact_integral": 0.0004556895348759296, + "original_integral": 0.00045568953487592936, + "original_abs_error": 2.168404344971009e-19, + "original_rel_error": 4.758512493734137e-16, + "reduced_integral": 0.0004556895348759296, + "reduced_abs_error": 0.0, + "reduced_rel_error": 0.0 + } + } + ] + } +] \ No newline at end of file diff --git a/notebooks/quad_plots/quadrature_plots.ipynb b/notebooks/quad_plots/quadrature_plots.ipynb new file mode 100644 index 0000000..f8d26b0 --- /dev/null +++ b/notebooks/quad_plots/quadrature_plots.ipynb @@ -0,0 +1,290 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib import rc\n", + "from matplotlib.ticker import MultipleLocator, LogLocator\n", + "\n", + "FOR_PRINT = True\n", + "\n", + "if FOR_PRINT:\n", + " LINE_WIDTH = 1\n", + " MARKER_SIZE = 3\n", + " FONT_SIZE = 8\n", + "\n", + " AXES_WIDTH = 0.65 * LINE_WIDTH\n", + "\n", + " plt.rcParams['grid.linewidth']=AXES_WIDTH\n", + " plt.rcParams['axes.linewidth']=AXES_WIDTH\n", + " plt.rcParams['axes.labelpad']=3.0\n", + "\n", + " plt.rcParams['xtick.major.pad']=2.0\n", + " plt.rcParams['xtick.major.size']=2.0\n", + " plt.rcParams['xtick.major.width']=AXES_WIDTH\n", + " plt.rcParams['xtick.minor.size']=1.0\n", + " plt.rcParams['xtick.minor.width']=0.75 * AXES_WIDTH\n", + "\n", + " plt.rcParams['ytick.major.pad']=-3.0\n", + " plt.rcParams['ytick.major.size']=2.0\n", + " plt.rcParams['ytick.major.width']=AXES_WIDTH\n", + " plt.rcParams['ytick.minor.size']=1.0\n", + " plt.rcParams['ytick.minor.width']=0.75 * AXES_WIDTH\n", + "else:\n", + " LINE_WIDTH = 6\n", + " MARKER_SIZE = 14\n", + " FONT_SIZE = 45\n", + "\n", + "%matplotlib inline\n", + "#plt.rcParams['figure.figsize'] = [15, 15]\n", + "plt.rcParams['lines.linewidth'] = LINE_WIDTH\n", + "plt.rcParams['lines.markeredgewidth'] = 0.75 * LINE_WIDTH\n", + "plt.rcParams['lines.markersize'] = MARKER_SIZE\n", + "plt.rcParams['font.size'] = FONT_SIZE\n", + "rc('text', usetex=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "data = dict()\n", + "with open(\"quad_reduc_results.json\") as results_json_file:\n", + " data = json.load(results_json_file)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "original_strength = []\n", + "original_rel_error = []\n", + "original_np = []\n", + "\n", + "reduced_strength = []\n", + "reduced_rel_error = []\n", + "reduced_np = []\n", + "\n", + "for result in data:\n", + " o_s = result[\"original_strength\"]\n", + " r_s = result[\"reduced_strength\"]\n", + "\n", + " if o_s == r_s:\n", + " original_strength.append(o_s)\n", + " original_rel_error.append(result[\"original_rel_error\"])\n", + " original_np.append(result[\"original_points\"])\n", + "\n", + " reduced_strength.append(r_s)\n", + " reduced_rel_error.append(result[\"reduced_rel_error\"])\n", + " reduced_np.append(result[\"reduced_points\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/svg+xml": "\r\n\r\n\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n", + "image/png": "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\n" + }, + "metadata": { + "needs_background": "light" + } + } + ], + "source": [ + "plt.plot(original_strength, original_rel_error, marker='x')\n", + "plt.plot(reduced_strength, reduced_rel_error, marker='x')\n", + "plt.legend(['Original', 'Reduced'], loc='lower left')\n", + "plt.semilogy()\n", + "plt.xlabel(\"Quadrature order\")\n", + "plt.ylabel(\"Relative error\")\n", + "plt.axes().xaxis.set_major_locator(MultipleLocator(1))\n", + "plt.title(\"Quadrature reduction: relative error\")\n", + "plt.grid()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/svg+xml": "\r\n\r\n\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n", + "image/png": "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\n" + }, + "metadata": { + "needs_background": "light" + } + } + ], + "source": [ + "plt.plot(original_strength, original_np, marker='x')\n", + "plt.plot(reduced_strength, reduced_np, marker='x')\n", + "plt.legend(['Original', 'Reduced'], loc='upper left')\n", + "#plt.semilogy()\n", + "plt.xlabel(\"Quadrature order\")\n", + "plt.ylabel(\"Number of points\")\n", + "plt.axes().yaxis.set_minor_locator(MultipleLocator(20))\n", + "plt.axes().xaxis.set_major_locator(MultipleLocator(1))\n", + "plt.title(\"Quadrature reduction: number of points\")\n", + "plt.grid()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "monomial_data = dict()\n", + "with open(\"quad_reduc_results_monomials.json\") as results_json_file:\n", + " monomial_data = json.load(results_json_file)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "original_errors = []\n", + "original_strengths = []\n", + "\n", + "original_intermediate_errors = []\n", + "original_intermediate_strengths = []\n", + "\n", + "reduced_errors = []\n", + "reduced_strengths = []\n", + "\n", + "for strength_set in monomial_data:\n", + " strength = strength_set[\"strength\"]\n", + " monomial_results = strength_set[\"results\"]\n", + "\n", + " original_errors_s = []\n", + " original_intermediate_errors_s = []\n", + " reduced_errors_s = []\n", + " for r in map(lambda r : r[\"result\"], monomial_results):\n", + " reduced_errors_s.append(r[\"reduced_rel_error\"])\n", + " if r[\"original_strength\"] == r[\"reduced_strength\"]:\n", + " original_errors_s.append(r[\"original_rel_error\"])\n", + " else:\n", + " original_intermediate_errors_s.append(r[\"original_rel_error\"])\n", + "\n", + " original_strengths_s = [strength + 0.1 for _ in range(len(original_errors_s))]\n", + " original_intermediate_strengths_s = [strength + 0.1 for _ in range(len(original_intermediate_errors_s))]\n", + " reduced_strengths_s = [strength - 0.1 for _ in range(len(reduced_errors_s))]\n", + "\n", + " original_errors.append(original_errors_s)\n", + " original_strengths.append(original_strengths_s)\n", + "\n", + " original_intermediate_errors.append(original_intermediate_errors_s)\n", + " original_intermediate_strengths.append(original_intermediate_strengths_s)\n", + "\n", + " reduced_errors.append(reduced_errors_s)\n", + " reduced_strengths.append(reduced_strengths_s)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/svg+xml": "\r\n\r\n\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n", + "image/png": "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\n" + }, + "metadata": { + "needs_background": "light" + } + } + ], + "source": [ + "FIG_WIDTH = 3.6 if FOR_PRINT else 20\n", + "FIG_HEIGHT = 1.9 if FOR_PRINT else 10\n", + "\n", + "fig = plt.figure(figsize=(FIG_WIDTH, FIG_HEIGHT))\n", + "\n", + "SCATTER_MARKER_SIZE = 0.35 * MARKER_SIZE ** 2 if FOR_PRINT else 160\n", + "\n", + "for (errs, s) in zip(reduced_errors, reduced_strengths):\n", + " errs = np.array(errs)\n", + " errs[errs < 1e-16] = np.NaN\n", + " scatter_reduced = plt.scatter(s, errs, s=SCATTER_MARKER_SIZE, color=\"#ff7d0e\", zorder=100)\n", + "for (errs, s) in zip(original_intermediate_errors, original_intermediate_strengths):\n", + " errs = np.array(errs)\n", + " errs[errs < 1e-16] = np.NaN\n", + " scatter_original = plt.scatter(s, errs, s=SCATTER_MARKER_SIZE, color=\"#7f9eb5\", zorder=100)\n", + "for (errs, s) in zip(original_errors, original_strengths):\n", + " errs = np.array(errs)\n", + " errs[errs < 1e-16] = np.NaN\n", + " scatter_original = plt.scatter(s, errs, s=SCATTER_MARKER_SIZE, color=\"#1f77b4\", zorder=100)\n", + "\n", + "plt.legend([scatter_original, scatter_reduced], ['Initial quadratures', 'Simplified quadratures'], prop={'size': 0.75 * FONT_SIZE}, loc='upper left')\n", + "plt.semilogy()\n", + "plt.ylim(5e-17, 5e-14)\n", + "plt.xlabel(\"Quadrature strength $m$\")\n", + "plt.ylabel(\"Relative error\")\n", + "plt.axes().xaxis.set_major_locator(MultipleLocator(1))\n", + "#plt.axes().yaxis.set_major_locator(LogLocator(10, subs=(1.0, 0.2)))\n", + "#plt.title(\"Monomial integration accuracy\", fontsize=45)\n", + "plt.grid(zorder=-100)\n", + "\n", + "plt.tight_layout()\n", + "#plt.savefig(\"monomial_accuracy.pdf\", format='pdf', bbox_inches='tight')\n", + "plt.savefig(\"monomial_accuracy.pgf\", format='pgf', bbox_inches='tight')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.3-final" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/notebooks/quadrature-simplification.ipynb b/notebooks/quadrature-simplification.ipynb new file mode 100644 index 0000000..28a0a81 --- /dev/null +++ b/notebooks/quadrature-simplification.ipynb @@ -0,0 +1,497 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 59, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from scipy.optimize import linprog\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [], + "source": [ + "v_t = np.loadtxt('../data/quadrature_debug/v_t.txt')\n", + "b = np.loadtxt('../data/quadrature_debug/b.txt')\n", + "w0 = np.loadtxt('../data/quadrature_debug/w0.txt')\n", + "#w = np.loadtxt('../w.txt')\n", + "#points = np.loadtxt('../points.txt')\n", + "# r = v_t.dot(w) - b\n", + "# np.linalg.norm(r)" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": {}, + "outputs": [], + "source": [ + "import gurobipy as gp\n", + "from gurobipy import GRB" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 9.0.1 build v9.0.1rc0 (linux64)\n", + "Optimize a model with 29365 rows, 58158 columns and 8374752 nonzeros\n", + "Model fingerprint: 0xf83f86e1\n", + "Variable types: 29079 continuous, 29079 integer (29079 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e-11, 4e+00]\n", + " Objective range [1e+00, 1e+00]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [2e-06, 1e-02]\n", + "Presolve removed 0 rows and 0 columns (presolve time = 5s) ...\n", + "Presolve time: 9.01s\n", + "Presolved: 29365 rows, 58158 columns, 8374705 nonzeros\n", + "Variable types: 29079 continuous, 29079 integer (29079 binary)\n", + "\n", + "Deterministic concurrent LP optimizer: primal simplex, dual simplex, and barrier\n", + "Showing barrier log only...\n", + "\n", + "Root barrier log...\n", + "\n", + "Ordering time: 0.00s\n", + "\n", + "Barrier statistics:\n", + " AA' NZ : 4.076e+04\n", + " Factor NZ : 4.104e+04 (roughly 10 MBytes of memory)\n", + " Factor Ops : 7.839e+06 (less than 1 second per iteration)\n", + " Threads : 2\n", + "\n", + " Objective Residual\n", + "Iter Primal Dual Primal Dual Compl Time\n", + " 0 8.49672223e+04 -1.57739798e+03 5.14e+03 1.11e-16 2.11e-01 16s\n", + " 1 1.43296800e+04 -8.35300116e+02 8.67e+02 2.22e-15 4.09e-02 16s\n", + " 2 1.68597921e+03 -1.64554351e+02 1.02e+02 2.66e-15 5.50e-03 16s\n", + " 3 1.08278760e+01 -2.40801260e-01 5.95e-01 2.22e-15 3.66e-05 16s\n", + " 4 1.08369548e+00 9.58671171e-01 5.07e-03 2.22e-15 9.30e-07 16s\n", + " 5 1.00000000e+00 9.98681157e-01 7.15e-16 2.22e-15 2.72e-08 17s\n", + " 6 1.00000000e+00 9.99998681e-01 8.81e-16 3.55e-15 2.72e-11 17s\n", + " 7 1.00000000e+00 9.99999879e-01 7.77e-16 2.22e-15 2.50e-12 17s\n", + " 8 9.99999999e-01 9.99999910e-01 3.54e-16 2.66e-15 1.83e-12 17s\n", + " 9 9.99999999e-01 9.99999958e-01 1.01e-15 2.22e-15 8.39e-13 17s\n", + " 10 9.99999998e-01 9.99999986e-01 8.60e-16 2.22e-15 2.54e-13 18s\n", + "\n", + "Barrier solved model in 10 iterations and 17.52 seconds\n", + "Optimal objective 9.99999998e-01\n", + "\n", + "\n", + "Root crossover log...\n", + "\n", + " 116 DPushes remaining with DInf 0.0000000e+00 18s\n", + "Concurrent spin time: 2.18s (can be avoided by choosing Method=3)\n", + "\n", + "Solved with primal simplex\n", + "\n", + "Root relaxation: objective 1.000000e+00, 675 iterations, 7.55 seconds\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 1.00000 0 282 - 1.00000 - - 46s\n", + "H 0 0 282.0000117 1.00000 100% - 47s\n", + "H 0 0 282.0000000 1.00000 100% - 48s\n", + " 0 0 1.00000 0 288 282.00000 1.00000 100% - 113s\n", + " 0 0 1.01091 0 286 282.00000 1.01091 100% - 191s\n", + " 0 0 1.01324 0 289 282.00000 1.01324 100% - 224s\n", + " 0 0 1.10081 0 287 282.00000 1.10081 100% - 253s\n", + " 0 0 1.10119 0 288 282.00000 1.10119 100% - 276s\n", + " 0 0 1.10119 0 288 282.00000 1.10119 100% - 544s\n", + " 0 0 1.10185 0 289 282.00000 1.10185 100% - 565s\n", + " 0 0 1.10201 0 291 282.00000 1.10201 100% - 664s\n", + " 0 0 1.10278 0 291 282.00000 1.10278 100% - 687s\n", + " 0 0 1.17166 0 288 282.00000 1.17166 100% - 710s\n", + " 0 0 1.17189 0 288 282.00000 1.17189 100% - 732s\n", + " 0 0 1.17192 0 292 282.00000 1.17192 100% - 741s\n", + " 0 0 1.17229 0 292 282.00000 1.17229 100% - 764s\n", + " 0 0 1.17237 0 291 282.00000 1.17237 100% - 783s\n", + " 0 0 1.17270 0 295 282.00000 1.17270 100% - 805s\n", + " 0 0 1.17275 0 296 282.00000 1.17275 100% - 828s\n", + " 0 0 1.17298 0 295 282.00000 1.17298 100% - 850s\n", + " 0 0 1.17308 0 296 282.00000 1.17308 100% - 869s\n", + " 0 0 1.17323 0 298 282.00000 1.17323 100% - 890s\n", + " 0 0 1.17324 0 296 282.00000 1.17324 100% - 915s\n", + " 0 0 1.17332 0 298 282.00000 1.17332 100% - 937s\n", + " 0 0 1.17336 0 300 282.00000 1.17336 100% - 956s\n", + " 0 0 1.17360 0 296 282.00000 1.17360 100% - 979s\n", + " 0 0 1.17360 0 296 282.00000 1.17360 100% - 1005s\n", + " 0 0 1.17384 0 301 282.00000 1.17384 100% - 1027s\n", + " 0 0 1.21168 0 290 282.00000 1.21168 100% - 1061s\n", + " 0 0 1.21171 0 289 282.00000 1.21171 100% - 1083s\n", + " 0 0 1.21172 0 291 282.00000 1.21172 100% - 1097s\n", + " 0 0 1.21188 0 295 282.00000 1.21188 100% - 1119s\n", + " 0 0 1.21193 0 292 282.00000 1.21193 100% - 1144s\n", + " 0 0 1.21208 0 295 282.00000 1.21208 100% - 1166s\n", + " 0 0 1.21216 0 295 282.00000 1.21216 100% - 1186s\n", + " 0 0 1.21235 0 300 282.00000 1.21235 100% - 1207s\n", + " 0 0 1.21236 0 299 282.00000 1.21236 100% - 1230s\n", + " 0 0 1.21264 0 297 282.00000 1.21264 100% - 1254s\n", + " 0 0 1.21265 0 298 282.00000 1.21265 100% - 1276s\n", + " 0 0 1.21303 0 298 282.00000 1.21303 100% - 1298s\n", + " 0 0 1.21305 0 299 282.00000 1.21305 100% - 1319s\n", + " 0 0 1.21370 0 303 282.00000 1.21370 100% - 1343s\n", + " 0 0 1.21372 0 303 282.00000 1.21372 100% - 1370s\n", + " 0 0 1.21427 0 300 282.00000 1.21427 100% - 1394s\n", + " 0 0 1.24214 0 296 282.00000 1.24214 100% - 1429s\n", + " 0 0 1.24248 0 295 282.00000 1.24248 100% - 1452s\n", + " 0 0 1.24257 0 296 282.00000 1.24257 100% - 1474s\n", + " 0 0 1.24317 0 300 282.00000 1.24317 100% - 1500s\n", + " 0 0 1.24322 0 298 282.00000 1.24322 100% - 1522s\n", + " 0 0 1.24385 0 299 282.00000 1.24385 100% - 1546s\n", + " 0 0 1.24393 0 298 282.00000 1.24393 100% - 1580s\n", + " 0 0 1.24444 0 298 282.00000 1.24444 100% - 1603s\n", + " 0 0 1.24462 0 303 282.00000 1.24462 100% - 1624s\n", + " 0 0 1.24499 0 305 282.00000 1.24499 100% - 1646s\n", + " 0 0 1.24505 0 302 282.00000 1.24505 100% - 1673s\n", + " 0 0 1.24548 0 302 282.00000 1.24548 100% - 1696s\n", + " 0 0 1.24551 0 303 282.00000 1.24551 100% - 1725s\n", + " 0 0 1.24578 0 303 282.00000 1.24578 100% - 1747s\n", + " 0 0 1.24580 0 301 282.00000 1.24580 100% - 1777s\n", + " 0 0 1.24610 0 303 282.00000 1.24610 100% - 1799s\n", + " 0 0 1.26958 0 294 282.00000 1.26958 100% - 1844s\n", + " 0 0 1.27022 0 296 282.00000 1.27022 100% - 1868s\n", + " 0 0 1.27027 0 300 282.00000 1.27027 100% - 1892s\n", + " 0 0 1.27094 0 299 282.00000 1.27094 100% - 1913s\n", + " 0 0 1.27107 0 299 282.00000 1.27107 100% - 1940s\n", + " 0 0 1.27206 0 300 282.00000 1.27206 100% - 1962s\n", + " 0 0 1.27206 0 301 282.00000 1.27206 100% - 1984s\n", + " 0 0 1.27284 0 297 282.00000 1.27284 100% - 2006s\n", + " 0 0 1.28897 0 299 282.00000 1.28897 100% - 2041s\n", + " 0 0 1.28980 0 299 282.00000 1.28980 100% - 2065s\n", + " 0 0 1.30509 0 299 282.00000 1.30509 100% - 2103s\n", + " 0 0 1.30523 0 298 282.00000 1.30523 100% - 2126s\n", + " 0 0 1.30570 0 298 282.00000 1.30570 100% - 3924s\n", + " 0 0 1.30662 0 301 282.00000 1.30662 100% - 3947s\n", + " 0 0 1.30674 0 303 282.00000 1.30674 100% - 4423s\n", + " 0 0 1.30802 0 304 282.00000 1.30802 100% - 4446s\n", + " 0 0 1.31852 0 301 282.00000 1.31852 100% - 4481s\n", + " 0 0 1.32045 0 302 282.00000 1.32045 100% - 4505s\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 0 0 1.32048 0 303 282.00000 1.32048 100% - 4531s\n", + " 0 0 1.32213 0 305 282.00000 1.32213 100% - 4556s\n", + " 0 0 1.33046 0 306 282.00000 1.33046 100% - 4587s\n", + " 0 0 1.33268 0 307 282.00000 1.33268 100% - 4611s\n", + " 0 0 1.33270 0 307 282.00000 1.33270 100% - 4643s\n", + " 0 0 1.33454 0 309 282.00000 1.33454 100% - 4665s\n", + " 0 0 1.33951 0 310 282.00000 1.33951 100% - 4700s\n", + " 0 0 1.34221 0 309 282.00000 1.34221 100% - 4723s\n", + " 0 0 1.37753 0 309 282.00000 1.37753 100% - 4762s\n", + " 0 0 1.38076 0 310 282.00000 1.38076 100% - 4784s\n", + " 0 0 1.38240 0 307 282.00000 1.38240 100% - 4814s\n", + " 0 0 1.38428 0 306 282.00000 1.38428 100% - 4838s\n", + " 0 0 1.39869 0 311 282.00000 1.39869 100% - 4875s\n", + " 0 0 1.40071 0 312 282.00000 1.40071 100% - 4900s\n", + " 0 0 1.40098 0 309 282.00000 1.40098 100% - 4928s\n", + " 0 0 1.40328 0 311 282.00000 1.40328 100% - 4953s\n", + " 0 0 1.40601 0 310 282.00000 1.40601 100% - 4986s\n", + " 0 0 1.40733 0 312 282.00000 1.40733 100% - 5011s\n", + " 0 0 1.40735 0 314 282.00000 1.40735 100% - 5039s\n", + " 0 0 1.40809 0 314 282.00000 1.40809 100% - 5064s\n", + " 0 0 1.40994 0 313 282.00000 1.40994 100% - 5104s\n", + " 0 0 1.41068 0 316 282.00000 1.41068 99.5% - 5129s\n", + " 0 0 1.42331 0 310 282.00000 1.42331 99.5% - 5170s\n", + " 0 0 1.42546 0 310 282.00000 1.42546 99.5% - 5194s\n", + " 0 0 1.43288 0 311 282.00000 1.43288 99.5% - 5232s\n", + " 0 0 1.43509 0 311 282.00000 1.43509 99.5% - 5257s\n", + " 0 0 1.43595 0 310 282.00000 1.43595 99.5% - 5288s\n", + " 0 0 1.43765 0 307 282.00000 1.43765 99.5% - 5314s\n", + " 0 0 1.44088 0 311 282.00000 1.44088 99.5% - 5346s\n", + " 0 0 1.44172 0 309 282.00000 1.44172 99.5% - 5370s\n", + " 0 0 1.45130 0 314 282.00000 1.45130 99.5% - 5408s\n", + " 0 0 1.45415 0 313 282.00000 1.45415 99.5% - 5434s\n", + " 0 0 1.45425 0 314 282.00000 1.45425 99.5% - 5465s\n", + " 0 0 1.45603 0 315 282.00000 1.45603 99.5% - 5490s\n", + " 0 0 1.46346 0 313 282.00000 1.46346 99.5% - 5529s\n", + " 0 0 1.46492 0 314 282.00000 1.46492 99.5% - 5553s\n", + " 0 0 1.46582 0 317 282.00000 1.46582 99.5% - 5581s\n", + " 0 0 1.46694 0 316 282.00000 1.46694 99.5% - 5606s\n", + " 0 0 1.46742 0 317 282.00000 1.46742 99.5% - 5641s\n", + " 0 0 1.46839 0 315 282.00000 1.46839 99.5% - 5665s\n", + " 0 0 1.46842 0 317 282.00000 1.46842 99.5% - 5701s\n", + " 0 0 1.46943 0 314 282.00000 1.46943 99.5% - 5724s\n", + " 0 0 1.46944 0 316 282.00000 1.46944 99.5% - 5756s\n", + " 0 0 1.47017 0 317 282.00000 1.47017 99.5% - 5781s\n", + " 0 0 1.47936 0 320 282.00000 1.47936 99.5% - 5816s\n", + " 0 0 1.48131 0 322 282.00000 1.48131 99.5% - 5841s\n", + " 0 0 1.48981 0 315 282.00000 1.48981 99.5% - 5882s\n", + " 0 0 1.49219 0 316 282.00000 1.49219 99.5% - 5908s\n", + " 0 0 1.49850 0 317 282.00000 1.49850 99.5% - 5951s\n", + " 0 0 1.50004 0 316 282.00000 1.50004 99.5% - 5976s\n", + " 0 0 1.50066 0 313 282.00000 1.50066 99.5% - 6012s\n", + " 0 0 1.50171 0 317 282.00000 1.50171 99.5% - 6038s\n", + " 0 0 1.50182 0 316 282.00000 1.50182 99.5% - 6073s\n", + " 0 0 1.50263 0 318 282.00000 1.50263 99.5% - 6099s\n", + " 0 0 1.50554 0 322 282.00000 1.50554 99.5% - 6136s\n", + " 0 0 1.50693 0 324 282.00000 1.50693 99.5% - 6165s\n" + ] + } + ], + "source": [ + "try:\n", + " num_vars = v_t.shape[1]\n", + " num_constraints = v_t.shape[0]\n", + " V = sum(w0)\n", + " \n", + " # Create a new model\n", + " model = gp.Model(\"milp\")\n", + "\n", + " w_vars = [ model.addVar(vtype=GRB.CONTINUOUS, name=\"w_{}\".format(i)) for i in range(0, num_vars)]\n", + " b_vars = [ model.addVar(vtype=GRB.BINARY, obj=1, name=\"b_{}\".format(i)) for i in range(0, num_vars)]\n", + " \n", + " model.addMConstrs(v_t, w_vars, '=', b)\n", + " \n", + " for i in range(0, num_vars):\n", + " model.addConstr(w_vars[i] <= V * b_vars[i])\n", + " \n", + "\n", + "# # TODO: Add P constraints\n", + "# def create_V_t_row_constraint(V_t, b, i):\n", + "# constraint = solver.Constraint(b[i], b[i], 'V_t_{}'.format(i))\n", + "# for j in range(0, num_vars):\n", + "# constraint.SetCoefficient(w_vars[j], V_t[i, j])\n", + "\n", + "# for j in range(0, num_vars):\n", + "# constraint = solver.Constraint(-solver.infinity(), 0, \"w_b_{}\".format(j))\n", + "# constraint.SetCoefficient(w_vars[j], 1)\n", + "# constraint.SetCoefficient(b_vars[j], -V)\n", + "\n", + "# P_constraints = [ create_V_t_row_constraint(v_t, b, i) for i in range(0, num_constraints) ]\n", + "# objective = solver.Objective()\n", + "# for b_i in b_vars:\n", + "# objective.SetCoefficient(b_i, 1)\n", + "\n", + "# solver.EnableOutput()\n", + "# print(solver.Solve())\n", + "# objective.Value()\n", + "\n", + "# w_glop = np.array([ w_i.solution_value() for w_i in w_vars ])\n", + "# b_glop = np.array([ b_i.solution_value() for b_i in b_vars ])\n", + "\n", + "# r = v_t.dot(w_glop) - b\n", + "# print(np.linalg.norm(r))\n", + "\n", + "\n", + "# # Create variables\n", + "# x = m.addVar(vtype=GRB.BINARY, name=\"x\")\n", + "# y = m.addVar(vtype=GRB.BINARY, name=\"y\")\n", + "# z = m.addVar(vtype=GRB.BINARY, name=\"z\")\n", + "\n", + "# # Set objective\n", + "# m.setObjective(x + y + 2 * z, GRB.MAXIMIZE)\n", + "\n", + "# # Add constraint: x + 2 y + 3 z <= 4\n", + "# m.addConstr(x + 2 * y + 3 * z <= 4, \"c0\")\n", + "\n", + "# # Add constraint: x + y >= 1\n", + "# m.addConstr(x + y >= 1, \"c1\")\n", + "\n", + " # Optimize model\n", + " model.optimize()\n", + " \n", + " w_gurobi = np.array([w.x for w in w_vars])\n", + " b_gurobi = np.array([b.x for b in b_vars])\n", + "\n", + "# for v in m.getVars():\n", + "# print('%s %g' % (v.varName, v.x))\n", + "\n", + "# print('Obj: %g' % m.objVal)\n", + "\n", + "except gp.GurobiError as e:\n", + " print('Error code ' + str(e.errno) + ': ' + str(e))\n", + "\n", + "except AttributeError as e:\n", + " print('Encountered an attribute error: ' + str(e))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "num_nonzero_gurobi = len([w_i for w_i in w_gurobi if w_i > 1e-14])\n", + "print(\"Num nonzero gurobi: {}\".format(num_nonzero_gurobi))" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABIcAAAReCAYAAABNQFB8AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nOzdf5DdZX3o8c9TonAFi9UybaHWpFOcQi2lyARbJaB2bu2UgrQw2h8zKki9VMrYXudWrdOLOtNeHJSWq8gwDRClBSm9VlSE8YqmwPVGE4gaUDByI1BCCQaE8CPJJt/7R5btsiRnzzn7Pft8v8/zevGHm92zu8+633Nmvu99fqSmaQIAAACAOv1Y7gEAAAAAkI84BAAAAFAxcQgAAACgYuIQAAAAQMXEIQAAAICKiUMAAAAAFVuSewBz/eRP/mSzdOnS3MMAAAAAKMa6desebprmkL19rHNxaOnSpbF27drcwwAAAAAoRkrpB/v6mGVlAAAAABUThwAAAAAqJg4BAAAAVEwcAgAAAKiYOAQAAABQMXEIAAAAoGLiEAAAAEDFxCEAAACAiolDPbRp06Z4xStesdePvf3tb48777xzkUcEAAAA9NWS3ANg73bt2hX77bffyJ/393//9xMYDQAAAFCq3sWhd93wrlj/4PrWv+7RP310/O0b/nafH//whz8cBxxwQJx77rnxZ3/2Z/HNb34zbrrppvjyl78cl19+eVx55ZXP+ZyVK1fG+eefH4ceemgcfvjhsf/++8fHPvaxeOtb3xonnXRSnHbaaRERcdBBB8W2bdviq1/9anzgAx+In/mZn4n169fHnXfeGR/96Efjsssui4g9s4Le9a53RUTE1NRUvOUtb4nbb789Xv7yl8cnP/nJeMELXhAnnnhiXHDBBXHssce2/v8RAAAAUJ7exaH1D66P1T9Yvejfd8WKFfGRj3wkzj333Fi7dm1s3749du7cGbfcckscf/zxz3n8Aw88EB/60Ifitttuixe+8IXxute9Ln7lV35l3u/z9a9/PTZs2BDLli2LdevWxeWXXx5r1qyJpmniuOOOixNOOCF+4id+Iu66665YuXJlvPrVr44zzjgjLr744nj3u989iR8dAAAAKFjv4tDRP310lq/7yle+MtatWxePP/547L///nHMMcfE2rVr4+abb46L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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure(figsize=(20,20))\n", + "plt.plot(w_gurobi, color=\"green\", linewidth=2.5, linestyle=\"-\", label=\"w_gurobi\")\n", + "# plt.plot(b_gurobi, color=\"blue\", linewidth=2.5, linestyle=\"-\", label=\"b_gurobi\")\n", + "plt.legend(loc='upper left', frameon=False)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# Try the same thing with Google's GLOP solver\n", + "from ortools.linear_solver import pywraplp" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# num_vars = v_t.shape[1]\n", + "# num_constraints = v_t.shape[0]\n", + "# V = sum(w0)\n", + "# solver = pywraplp.Solver('quadrature_lp', pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING)\n", + "\n", + "# w_vars = [ solver.NumVar(0.0, float(\"inf\"), \"w_{}\".format(i)) for i in range(0, num_vars)]\n", + "# b_vars = [ solver.IntVar(0.0, 1.0, \"b_{}\".format(i)) for i in range(0, num_vars)]\n", + "\n", + "# # TODO: Add P constraints\n", + "# def create_V_t_row_constraint(V_t, b, i):\n", + "# constraint = solver.Constraint(b[i], b[i], 'V_t_{}'.format(i))\n", + "# for j in range(0, num_vars):\n", + "# constraint.SetCoefficient(w_vars[j], V_t[i, j])\n", + " \n", + "# for j in range(0, num_vars):\n", + "# constraint = solver.Constraint(-solver.infinity(), 0, \"w_b_{}\".format(j))\n", + "# constraint.SetCoefficient(w_vars[j], 1)\n", + "# constraint.SetCoefficient(b_vars[j], -V)\n", + " \n", + "# P_constraints = [ create_V_t_row_constraint(v_t, b, i) for i in range(0, num_constraints) ]\n", + "# objective = solver.Objective()\n", + "# for b_i in b_vars:\n", + "# objective.SetCoefficient(b_i, 1)\n", + "\n", + "# solver.EnableOutput()\n", + "# print(solver.Solve())\n", + "# objective.Value()\n", + "\n", + "# w_glop = np.array([ w_i.solution_value() for w_i in w_vars ])\n", + "# b_glop = np.array([ b_i.solution_value() for b_i in b_vars ])\n", + "\n", + "# r = v_t.dot(w_glop) - b\n", + "# print(np.linalg.norm(r))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure(figsize=(20,20))\n", + "plt.plot(w_glop, color=\"green\", linewidth=2.5, linestyle=\"-\", label=\"w_glop\")\n", + "plt.plot(b_glop, color=\"blue\", linewidth=2.5, linestyle=\"-\", label=\"b_glop\")\n", + "plt.legend(loc='upper left', frameon=False)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "num_nonzero_glop = len([w_i for w_i in w_glop if w_i > 1e-14])\n", + "print(\"Num nonzero glop: {}\".format(num_nonzero_glop))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# from bokeh.plotting import figure, show, output_file, output_notebook\n", + "# from math import sqrt, pi\n", + "\n", + "# output_notebook()\n", + "\n", + "# def plot_points(points, weights):\n", + "# x = points[:, 0]\n", + "# y = points[:, 1]\n", + "# print(sum(weights))\n", + "# radii = [ sqrt(w_i / pi) for w_i in weights ]\n", + "# colors = [\n", + "# \"#%02x%02x%02x\" % (int(r), int(g), 150) for r, g in zip(50+2*x, 30+2*y)\n", + "# ]\n", + "\n", + "# TOOLS=\"hover,crosshair,pan,wheel_zoom,zoom_in,zoom_out,box_zoom,undo,redo,reset,tap,save,box_select,poly_select,lasso_select,\"\n", + "\n", + "# p = figure(tools=TOOLS)\n", + "\n", + "# p.scatter(x, y, radius=radii,\n", + "# fill_color=colors, fill_alpha=0.6,\n", + "# line_color=None)\n", + "\n", + "# show(p)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/quadrature_lp.ipynb b/notebooks/quadrature_lp.ipynb new file mode 100644 index 0000000..48d465f --- /dev/null +++ b/notebooks/quadrature_lp.ipynb @@ -0,0 +1,242 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from scipy.optimize import linprog\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "P = np.loadtxt('../P.txt')\n", + "b = np.loadtxt('../b.txt')\n", + "w = np.loadtxt('../w.txt')\n", + "points = np.loadtxt('../points.txt')\n", + "r = P.dot(w) - b\n", + "np.linalg.norm(r)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "volume = sum(w)\n", + "centroid = points.transpose().dot(w) / volume\n", + "\n", + "# Set up linprog constraints\n", + "A_eq = P.copy()\n", + "b_eq = b.copy()\n", + "x0 = w.copy()\n", + "c = np.zeros(w.size)#np.ones(w.size)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Note: linprog has default non-negative bound x >= 0\n", + "result = linprog(c, A_eq = A_eq, b_eq=b_eq, x0=x0, method='simplex')\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "w_new = result.x.copy()\n", + "\n", + "# Check that w_new also satisfies constraints\n", + "r_new = P.dot(w_new) - b\n", + "print(np.min(w_new))\n", + "np.linalg.norm(r_new)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure(figsize=(20,20))\n", + "plt.plot(w, color=\"blue\", linewidth=2.5, linestyle=\"-\", label=\"w\")\n", + "plt.plot(w_new, color=\"red\", linewidth=2.5, linestyle=\"-\", label=\"w_new\")\n", + "plt.legend(loc='upper left', frameon=False)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "num_nonzero_old = len([w_i for w_i in w if w_i > 1e-14])\n", + "num_nonzero_new = len([w_i for w_i in w_new if w_i > 1e-14])\n", + "print(\"Num nonzero old: {}\".format(num_nonzero_old))\n", + "print(\"Num nonzero new: {}\".format(num_nonzero_new))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Try the same thing with Google's GLOP solver\n", + "from ortools.linear_solver import pywraplp" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "solver = pywraplp.Solver('quadrature_lp', pywraplp.Solver.GLOP_LINEAR_PROGRAMMING)\n", + "\n", + "w_vars = [ solver.NumVar(0.0, float(\"inf\"), \"w_{}\".format(i)) for i in range(0, len(w))]\n", + "\n", + "# TODO: Add P constraints\n", + "def create_P_row_constraint(P, b, i):\n", + " constraint = solver.Constraint(b[i], b[i], 'P_{}'.format(i))\n", + " for j in range(0, P.shape[1]):\n", + " constraint.SetCoefficient(w_vars[j], P[i, j])\n", + " \n", + "P_constraints = [ create_P_row_constraint(P, b, i) for i in range(0, P.shape[0]) ]\n", + "objective = solver.Objective()\n", + "# for w_i in w_vars:\n", + "# objective.SetCoefficient(w_i, 1)\n", + "\n", + "solver.Solve()\n", + "objective.Value()\n", + "\n", + "w_glop = np.array([ w_i.solution_value() for w_i in w_vars ])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure(figsize=(20,20))\n", + "plt.plot(w, color=\"blue\", linewidth=2.5, linestyle=\"-\", label=\"w\")\n", + "plt.plot(w_new, color=\"red\", linewidth=2.5, linestyle=\"-\", label=\"w_linprog\")\n", + "plt.plot(w_new, color=\"green\", linewidth=2.5, linestyle=\"-\", label=\"w_glop\")\n", + "plt.legend(loc='upper left', frameon=False)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "num_nonzero_glop = len([w_i for w_i in w_glop if w_i > 1e-14])\n", + "print(\"Num nonzero glop: {}\".format(num_nonzero_glop))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from bokeh.plotting import figure, show, output_file, output_notebook\n", + "from math import sqrt, pi\n", + "\n", + "output_notebook()\n", + "\n", + "def plot_points(points, weights):\n", + " x = points[:, 0]\n", + " y = points[:, 1]\n", + " print(sum(weights))\n", + " radii = [ sqrt(w_i / pi) for w_i in weights ]\n", + " colors = [\n", + " \"#%02x%02x%02x\" % (int(r), int(g), 150) for r, g in zip(50+2*x, 30+2*y)\n", + " ]\n", + "\n", + " TOOLS=\"hover,crosshair,pan,wheel_zoom,zoom_in,zoom_out,box_zoom,undo,redo,reset,tap,save,box_select,poly_select,lasso_select,\"\n", + "\n", + " p = figure(tools=TOOLS)\n", + "\n", + " p.scatter(x, y, radius=radii,\n", + " fill_color=colors, fill_alpha=0.6,\n", + " line_color=None)\n", + "\n", + " show(p)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_points(points, w_new)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_points(points, w_glop)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_points(points, w)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/slingshot_iterations/iterations.ipynb b/notebooks/slingshot_iterations/iterations.ipynb new file mode 100644 index 0000000..228f8e4 --- /dev/null +++ b/notebooks/slingshot_iterations/iterations.ipynb @@ -0,0 +1,162 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "import re\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib import rc\n", + "from matplotlib.ticker import MultipleLocator, LogLocator\n", + "\n", + "%matplotlib inline\n", + "plt.rcParams['figure.figsize'] = [15, 15]\n", + "plt.rcParams.update({'font.size': 45})\n", + "rc('text', usetex=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "def read_iterations(filename):\n", + " cg_iters = []\n", + " newt_iters = []\n", + "\n", + " with open(filename) as logfile:\n", + " for line in logfile:\n", + " match = re.match(\".*CG iterations: (\\d+).*\", line)\n", + " if match:\n", + " cg_iters.append(match.group(1))\n", + " continue\n", + " \n", + " match = re.match(\".*Number of newton iterations in Backward Euler step: (\\d+).*\", line)\n", + " if match:\n", + " newt_iters.append(match.group(1))\n", + " continue\n", + "\n", + " cg_iters = np.array(cg_iters, dtype=np.int32)\n", + " newt_iters = np.array(newt_iters, dtype=np.int32)\n", + "\n", + " return (cg_iters, newt_iters)\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "def get_stats(filename):\n", + " (cg_iters, newt_iters) = read_iterations(filename)\n", + "\n", + " timesteps = len(newt_iters)\n", + " total_cg_iters = cg_iters.sum()\n", + " total_newt_iters = newt_iters.sum()\n", + " avg_cg_iters = total_cg_iters / timesteps\n", + " avg_newt_iters = total_newt_iters / timesteps\n", + "\n", + " print(f\"Filename: {filename}\")\n", + " print(f\"Timesteps: {timesteps}\")\n", + " print(f\"Total CG iters: {total_cg_iters}\")\n", + " print(f\"Total Newt iters: {total_newt_iters}\")\n", + " print(f\"Avg CG iters per timestep: {avg_cg_iters}\")\n", + " print(f\"Avg Newt iters per timestep: {avg_newt_iters}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "Filename: armadillo_slingshot_embedded_tet10_5000_femproto2_2020-05-21_11-35-43-891374.log\nTimesteps: 5000\nTotal CG iters: 1463478\nTotal Newt iters: 5809\nAvg CG iters per timestep: 292.6956\nAvg Newt iters per timestep: 1.1618\n" + } + ], + "source": [ + "get_stats(\"armadillo_slingshot_embedded_tet10_5000_femproto2_2020-05-21_11-35-43-891374.log\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "Filename: armadillo_slingshot_fem_tet10_5000_femproto2_2020-05-21_09-08-23-084417.log\nTimesteps: 5000\nTotal CG iters: 1155671\nTotal Newt iters: 5803\nAvg CG iters per timestep: 231.1342\nAvg Newt iters per timestep: 1.1606\n" + } + ], + "source": [ + "get_stats(\"armadillo_slingshot_fem_tet10_5000_femproto2_2020-05-21_09-08-23-084417.log\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "Filename: armadillo_slingshot_embedded_tet10_500_femproto2_2020-05-21_14-50-43-484014.log\nTimesteps: 5000\nTotal CG iters: 736848\nTotal Newt iters: 5659\nAvg CG iters per timestep: 147.3696\nAvg Newt iters per timestep: 1.1318\n" + } + ], + "source": [ + "get_stats(\"armadillo_slingshot_embedded_tet10_500_femproto2_2020-05-21_14-50-43-484014.log\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "Filename: armadillo_slingshot_fem_tet10_500_femproto2_2020-05-22_13-50-41-106940.log\nTimesteps: 5000\nTotal CG iters: 508117\nTotal Newt iters: 5856\nAvg CG iters per timestep: 101.6234\nAvg Newt iters per timestep: 1.1712\n" + } + ], + "source": [ + "get_stats(\"armadillo_slingshot_fem_tet10_500_femproto2_2020-05-22_13-50-41-106940.log\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6-final" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/notebooks/unit_tests_analytic_solutions.ipynb b/notebooks/unit_tests_analytic_solutions.ipynb new file mode 100644 index 0000000..bea9d80 --- /dev/null +++ b/notebooks/unit_tests_analytic_solutions.ipynb @@ -0,0 +1,180 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "L2 error squared estimation\n", + "----------------------------\n", + "\n", + "\n", + "### Bilinear quad" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "from sympy import *\n", + "from sympy.integrals.intpoly import polytope_integrate\n", + "from sympy.abc import x, y" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "points = [ Point2D(-1, -1), Point2D(2, -2), Point2D(4, 1), Point2D(-2, 3)]\n", + "\n", + "def phi_alpha_beta(alpha, beta, x, y):\n", + " return (1 + alpha * x) * (1 + beta * y) / 4\n", + "\n", + "# Define basis functions phi(x, y)\n", + "def phi_local(x, y):\n", + " return [\n", + " phi_alpha_beta(-1, -1, x, y),\n", + " phi_alpha_beta(1, -1, x, y),\n", + " phi_alpha_beta(1, 1, x, y),\n", + " phi_alpha_beta(-1, 1, x, y)\n", + " ]\n", + "\n", + "# Define transformation from reference element T: K_hat -> K,\n", + "# with K being the element defined by quad.\n", + "def T(x, y):\n", + " p = phi_local(x, y)\n", + " return points[0] * p[0] + points[1] * p[1] + points[2] * p[2] + points[3] * p[3]\n", + " \n", + "def u(x, y):\n", + " return 5 * x * y + 3 * x - 2 * y - 5\n", + "\n", + "def u_local(xi, eta):\n", + " (x, y) = T(xi, eta)\n", + " return u(x, y)\n", + "\n", + "u_h_weights = [u(p[0], p[1]) for p in points]\n", + "\n", + "def u_h_local(xi, eta):\n", + " p = phi_local(xi, eta)\n", + " u = u_h_weights\n", + " return u[0] * p[0] + u[1] * p[1] + u[2] * p[2] + u[3] * p[3]" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\displaystyle \\frac{9955}{12}$" + ], + "text/plain": [ + "9955/12" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "det_J_K = Matrix(T(x, y)).jacobian(Matrix([x, y])).det()\n", + "\n", + "integrand = expand(det_J_K * (u_h_local(x, y) - u_local(x, y))**2)\n", + "\n", + "# Note: It may be necessary to expand the polynomial for use with polytope_integrate\n", + "#integral = polytope_integrate(reference_quad, 1)\n", + "# Note: polytope_integrate did not seem to work so well. Since we anyway integrate in the reference domain,\n", + "# which is a simple square, we can just integrate normally with simple limits\n", + "integral = integrate(integrand, (x, -1, 1), (y, -1, 1))\n", + "integral" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\displaystyle \\frac{43 x y}{2} + \\frac{29 x}{2} - \\frac{3 y}{2} - \\frac{19}{2}$" + ], + "text/plain": [ + "43*x*y/2 + 29*x/2 - 3*y/2 - 19/2" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "expand(u_h_local(x, y))" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\displaystyle - \\frac{15 x^{2} y^{2}}{16} - \\frac{45 x^{2} y}{8} - \\frac{135 x^{2}}{16} + \\frac{25 x y^{2}}{4} + \\frac{43 x y}{2} + \\frac{33 x}{4} + \\frac{35 y^{2}}{16} + \\frac{33 y}{8} - \\frac{37}{16}$" + ], + "text/plain": [ + "-15*x**2*y**2/16 - 45*x**2*y/8 - 135*x**2/16 + 25*x*y**2/4 + 43*x*y/2 + 33*x/4 + 35*y**2/16 + 33*y/8 - 37/16" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "expand(u_local(x, y))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/paradis/Cargo.toml b/paradis/Cargo.toml new file mode 100644 index 0000000..f0cb35f --- /dev/null +++ b/paradis/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "paradis" +version = "0.1.0" +authors = ["Andreas Longva "] +edition = "2018" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rayon = "1.3" +nested-vec = { path = "../nested-vec" } +# TODO: Make serde optional +serde = { version = "1.0", features = [ "derive" ] } + +[dev-dependencies] +rand = "0.7" +proptest = "0.9" diff --git a/paradis/src/adapter.rs b/paradis/src/adapter.rs new file mode 100644 index 0000000..d1e0bf9 --- /dev/null +++ b/paradis/src/adapter.rs @@ -0,0 +1,230 @@ +use crate::{ParallelAccess, ParallelStorage}; +use std::marker::PhantomData; + +/// An adapter that facilitates blocked storage. +/// +/// Many problems naturally store data in blocks. As an example, consider a vector `x` consisting +/// of `n` 3-element vectors `x_i`, stored contiguously as +/// `x = vec![x_11, x_12, x_13, x_21, ..., x_n1, x_n2, x_n3]`. +/// In order to consider subsets of indices in the range `0 .. 3*n`, we may instead consider +/// subsets of indices in the range `0 .. n`, each index corresponding to a *block* of `x`. +/// This adapter facilitates these kind of storage patterns by transparently making +/// `DisjointSubsets` create parallel iterators over subsets of *blocks*. +/// +/// For illustration, see the below example. +/// +/// ```rust +/// use paradis::DisjointSubsets; +/// use paradis::adapter::BlockAdapter; +/// use rayon::iter::ParallelIterator; +/// +/// // 7 blocks of 3 +/// let mut data = vec![0; 21]; +/// +/// let subsets = vec![ +/// vec![1, 3, 5], +/// vec![0, 2], +/// vec![4, 6] +/// ]; +/// let subsets = DisjointSubsets::try_from_disjoint_subsets(&subsets).unwrap(); +/// +/// let mut adapter = BlockAdapter::with_block_size(data.as_mut_slice(), 3); +/// subsets.subsets_par_iter(&mut adapter) +/// .for_each(|mut subset| { +/// for i in 0 .. subset.len() { +/// // Each subset consists of blocks `subset.len()` blocks +/// let mut block_i = subset.get_mut(i); +/// *block_i.index_mut(0) += 1; +/// *block_i.index_mut(1) += 2; +/// *block_i.index_mut(2) += 3; +/// } +/// }); +/// +/// assert_eq!(data, vec![1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]); +/// ``` +/// +/// +/// +/// +/// +#[derive(Debug)] +pub struct BlockAdapter<'a, Storage: ?Sized> { + storage: &'a mut Storage, + block_size: usize, +} + +impl<'a, Storage> BlockAdapter<'a, Storage> +where + Storage: ?Sized + ParallelStorage<'a>, +{ + pub fn with_block_size(storage: &'a mut Storage, block_size: usize) -> Self { + assert!(block_size > 0); + assert_eq!( + storage.len() % block_size, + 0, + "Storage length must be divisible by block size." + ); + Self { storage, block_size } + } +} + +#[derive(Copy, Clone)] +pub struct Block<'a, Access> { + access: Access, + start_idx: usize, + block_size: usize, + marker: PhantomData<&'a Access>, +} + +impl<'a, 'b, Access> Block<'a, Access> +where + 'a: 'b, + Access: ParallelAccess<'b>, +{ + pub fn len(&self) -> usize { + self.block_size + } + + pub fn get(&'b self, index_in_block: usize) -> Option { + if index_in_block < self.block_size { + let global_index = self.start_idx + index_in_block; + Some(unsafe { self.access.get_unchecked(global_index) }) + } else { + None + } + } + + pub fn index(&'b self, index_in_block: usize) -> Access::Record { + self.get(index_in_block).expect("Index must be in bounds") + } +} + +#[derive(Copy, Clone)] +pub struct BlockMut<'a, Access> { + access: Access, + start_idx: usize, + block_size: usize, + marker: PhantomData<&'a mut Access>, +} + +impl<'a, 'b, Access> BlockMut<'a, Access> +where + 'a: 'b, + Access: ParallelAccess<'b>, +{ + pub fn len(&self) -> usize { + self.block_size + } + + pub fn get(&'b self, index_in_block: usize) -> Option { + if index_in_block < self.block_size { + let global_index = self.start_idx + index_in_block; + Some(unsafe { self.access.get_unchecked(global_index) }) + } else { + None + } + } + + pub fn get_mut(&'b mut self, index_in_block: usize) -> Option { + if index_in_block < self.block_size { + let global_index = self.start_idx + index_in_block; + Some(unsafe { self.access.get_unchecked_mut(global_index) }) + } else { + None + } + } + + pub fn index(&'b self, index_in_block: usize) -> Access::Record { + self.get(index_in_block).expect("Index must be in bounds") + } + + pub fn index_mut(&'b mut self, index_in_block: usize) -> Access::RecordMut { + self.get_mut(index_in_block) + .expect("Index must be in bounds") + } +} + +#[derive(Copy, Clone)] +pub struct BlockAccess { + access: Access, + block_size: usize, +} + +unsafe impl<'a, Access> ParallelAccess<'a> for BlockAccess +where + Access: 'a + ParallelAccess<'a>, +{ + type Record = Block<'a, Access>; + type RecordMut = BlockMut<'a, Access>; + + unsafe fn get_unchecked(&'a self, global_index: usize) -> Self::Record { + Block { + access: self.access.clone(), + start_idx: self.block_size * global_index, + block_size: self.block_size, + marker: PhantomData, + } + } + + unsafe fn get_unchecked_mut(&'a self, global_index: usize) -> Self::RecordMut { + BlockMut { + access: self.access.clone(), + start_idx: self.block_size * global_index, + block_size: self.block_size, + marker: PhantomData, + } + } +} + +unsafe impl<'a, Storage> ParallelStorage<'a> for BlockAdapter<'a, Storage> +where + Storage: ?Sized + ParallelStorage<'a>, +{ + type Access = BlockAccess; + + fn create_access(&'a mut self) -> Self::Access { + BlockAccess { + access: self.storage.create_access(), + block_size: self.block_size, + } + } + + fn len(&self) -> usize { + self.storage.len() / self.block_size + } +} + +#[cfg(test)] +mod tests { + use crate::adapter::BlockAdapter; + use crate::DisjointSubsets; + use rayon::iter::{IndexedParallelIterator, ParallelIterator}; + + #[test] + fn blocked_slice_parallel_test() { + // 7 blocks of 3 + let mut data = vec![0; 21]; + + let subsets = vec![vec![1, 3, 5], vec![0, 2], vec![4, 6]]; + let labels = vec![0, 1, 2]; + let subsets = DisjointSubsets::try_from_disjoint_subsets(&subsets, labels).unwrap(); + + let mut adapter = BlockAdapter::with_block_size(data.as_mut_slice(), 3); + subsets + .subsets_par_iter(&mut adapter) + .with_min_len(1) + .for_each(|mut subset| { + for i in 0..subset.len() { + let mut block_i = subset.get_mut(i); + *block_i.index_mut(0) += 1; + *block_i.index_mut(1) += 2; + *block_i.index_mut(2) += 3; + } + }); + + assert_eq!( + data, + vec![1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3] + ); + } +} diff --git a/paradis/src/coloring.rs b/paradis/src/coloring.rs new file mode 100644 index 0000000..d3746d5 --- /dev/null +++ b/paradis/src/coloring.rs @@ -0,0 +1,112 @@ +use crate::DisjointSubsets; +use nested_vec::NestedVec; +use std::collections::BTreeSet; + +#[derive(Debug)] +struct Color { + subsets: NestedVec, + labels: Vec, + indices: BTreeSet, +} + +impl Color { + fn new_with_subset(subset: &[usize], label: usize) -> Self { + let mut subsets = NestedVec::new(); + subsets.push(subset); + Self { + subsets, + labels: vec![label], + indices: subset.iter().copied().collect(), + } + } + + fn try_add_subset(&mut self, subset: &[usize], label: usize, local_workspace_set: &mut BTreeSet) -> bool { + local_workspace_set.clear(); + for idx in subset { + local_workspace_set.insert(*idx); + } + + if self.indices.is_disjoint(&local_workspace_set) { + self.subsets.push(subset); + self.labels.push(label); + + for &idx in local_workspace_set.iter() { + self.indices.insert(idx); + } + true + } else { + false + } + } + + fn max_index(&self) -> Option { + // Use the fact that the last element in a BTreeSet is the largest value in the set + self.indices.iter().copied().last() + } +} + +pub fn sequential_greedy_coloring(subsets: &NestedVec) -> Vec { + let mut colors = Vec::::new(); + let mut workspace_set = BTreeSet::new(); + + 'subset_loop: for (label, subset) in subsets.iter().enumerate() { + for color in &mut colors { + if color.try_add_subset(subset, label, &mut workspace_set) { + continue 'subset_loop; + } + } + + // We did not succeed in adding the subset to an existing color, + // so create a new one instead + colors.push(Color::new_with_subset(subset, label)); + } + + colors + .into_iter() + .map(|color| { + let max_index = color.max_index(); + // Subsets must be disjoint by construction, so skip checks + unsafe { DisjointSubsets::from_disjoint_subsets_unchecked(color.subsets, color.labels, max_index) } + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::sequential_greedy_coloring; + use crate::DisjointSubsets; + use nested_vec::NestedVec; + use proptest::collection::vec; + use proptest::prelude::*; + use proptest::proptest; + + proptest! { + #[test] + fn sequential_greedy_coloring_produces_disjoint_subsets( + integer_subsets in vec(vec(0 .. 100usize, 0 .. 10), 0 .. 10) + ) { + // Generate a random Vec>, which can be interpreted as a set of + // subsets, which we then color + let subsets = NestedVec::from(&integer_subsets); + let colors = sequential_greedy_coloring(&subsets); + + // There can not be more colors than there are original subsets + prop_assert!(colors.len() <= subsets.len()); + + let num_subsets_across_colors: usize = colors + .iter() + .map(|disjoint_subsets| disjoint_subsets.subsets().len()) + .sum(); + + prop_assert_eq!(num_subsets_across_colors, subsets.len()); + + // Actually assert that each color has disjoint subsets by running it through + // the `try...` constructor for DisjointSubsets + for subset in colors { + let checked_subsets = DisjointSubsets::try_from_disjoint_subsets( + subset.subsets().clone(), subset.labels().to_vec()); + prop_assert_eq!(checked_subsets, Ok(subset.clone())); + } + } + } +} diff --git a/paradis/src/lib.rs b/paradis/src/lib.rs new file mode 100644 index 0000000..37a08a8 --- /dev/null +++ b/paradis/src/lib.rs @@ -0,0 +1,632 @@ +//! paradis +//! ======= +//! +//! Parallel processing of disjoint subsets. + +pub mod adapter; +pub mod coloring; +pub mod slice; + +use nested_vec::NestedVec; +use rayon::iter::plumbing::{bridge, Consumer, Producer, ProducerCallback, UnindexedConsumer}; +use rayon::iter::{IndexedParallelIterator, ParallelIterator}; +use serde::{Deserialize, Serialize}; +use std::cmp::max; +use std::collections::HashSet; +use std::fmt; +use std::fmt::{Debug, Formatter}; + +pub struct SubsetAccess<'a, Access> { + subset_label: usize, + global_indices: &'a [usize], + access: Access, +} + +impl<'a, Access> SubsetAccess<'a, Access> { + pub fn global_indices(&self) -> &[usize] { + &self.global_indices + } + + pub fn label(&self) -> usize { + self.subset_label + } + + pub fn len(&self) -> usize { + self.global_indices().len() + } + + pub fn get<'b>(&'b self, local_index: usize) -> >::Record + where + 'a: 'b, + Access: ParallelAccess<'b>, + { + let global_index = self.global_indices[local_index]; + unsafe { self.access.get_unchecked(global_index) } + } + + pub fn get_mut<'b>(&'b mut self, local_index: usize) -> >::RecordMut + where + 'a: 'b, + Access: ParallelAccess<'b>, + { + let global_index = self.global_indices[local_index]; + unsafe { self.access.get_unchecked_mut(global_index) } + } +} + +// TODO: Does this trait need to be unsafe, or does it suffice to have unsafe methods? +// I suppose it cannot technically be `Sync`/`Send` without requiring some +// `Unsafe` in most cases though +pub unsafe trait ParallelAccess<'a>: Sync + Send + Clone { + type Record; + type RecordMut; + + unsafe fn get_unchecked(&'a self, global_index: usize) -> Self::Record; + unsafe fn get_unchecked_mut(&'a self, global_index: usize) -> Self::RecordMut; +} + +pub unsafe trait ParallelStorage<'a> { + // TODO: Can we do without the Clone bound? + type Access: Send + Clone; + + // TODO: should this be unsafe, since the ParallelAccess trait needs Sync + Send, + // which may not really be sound from a "safe" perspective? + fn create_access(&'a mut self) -> Self::Access; + fn len(&self) -> usize; +} + +/// A set of subsets of indices, in which the intersection of indices between any two subsets is +/// empty. +/// +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DisjointSubsets { + // Store the max global index present in any of the subsets. We need this to + // ensure that none of the indices are out of bounds when accessing a storage. + max_index: Option, + // Each subset consists of a set of indices. Indices are allowed to overlap within a subset, + // but the intersection between the indices of any two subsets must be empty. In other words, + // no two subsets share a common index. + subsets: NestedVec, + // Store a label for each subset + labels: Vec, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct SubsetsNotDisjointError; + +impl DisjointSubsets { + pub fn try_from_disjoint_subsets>>( + subsets: Subsets, + labels: Vec, + ) -> Result { + let subsets = subsets.into(); + assert_eq!(subsets.len(), labels.len(), "Must have exactly one label per subset."); + + let mut max_index = None; + let mut global_index_set = HashSet::new(); + // Subsets are allowed to contain duplicate entries, so we therefore build a local index + // set for each subset before checking against and adding them to the global index set. + let mut local_index_set = HashSet::new(); + + // Verify that subsets are disjoint + for subset in subsets.iter() { + local_index_set.clear(); + for idx in subset { + if let Some(ref mut current_max) = max_index { + *current_max = max(*current_max, *idx); + } else { + max_index = Some(*idx); + } + local_index_set.insert(*idx); + } + + for idx in &local_index_set { + let idx_already_present = !global_index_set.insert(*idx); + if idx_already_present { + return Err(SubsetsNotDisjointError); + } + } + } + + let disjoint_subsets = DisjointSubsets { + max_index, + subsets, + labels, + }; + + Ok(disjoint_subsets) + } + + pub unsafe fn from_disjoint_subsets_unchecked>>( + subsets: Subsets, + labels: Vec, + max_index: Option, + ) -> Self { + let subsets = subsets.into(); + assert_eq!(subsets.len(), labels.len(), "Must have exactly one label per subset."); + Self { + max_index, + subsets: subsets.into(), + labels, + } + } + + pub fn subsets(&self) -> &NestedVec { + &self.subsets + } + + pub fn into_subsets(self) -> NestedVec { + self.subsets + } + + pub fn labels(&self) -> &[usize] { + &self.labels + } + + /// Create a parallel iterator over the subsets, fetching data from the provided storage. + /// + /// Panics if any subset contains an index that exceeds the length reported by `storage`. + pub fn subsets_par_iter<'a, Storage>( + &'a self, + storage: &'a mut Storage, + ) -> DisjointSubsetsParIter<'a, Storage::Access> + where + Storage: ?Sized + ParallelStorage<'a>, + { + assert!( + self.max_index.is_none() || storage.len() > self.max_index.unwrap(), + "Subsets contain indices out of bounds." + ); + // Sanity check: if we don't have a max index, then we also cannot have any subsets + debug_assert_eq!(self.max_index.is_none(), self.subsets.len() == 0); + let access = storage.create_access(); + + DisjointSubsetsParIter { + access, + subsets: &self.subsets, + labels: &self.labels, + } + } +} + +pub struct DisjointSubsetsParIter<'a, Access> { + access: Access, + subsets: &'a NestedVec, + labels: &'a [usize], +} + +impl<'a, Access: Send + Clone> ParallelIterator for DisjointSubsetsParIter<'a, Access> { + type Item = SubsetAccess<'a, Access>; + + fn drive_unindexed(self, consumer: C) -> C::Result + where + C: UnindexedConsumer, + { + bridge(self, consumer) + } + + fn opt_len(&self) -> Option { + Some(self.len()) + } +} + +impl<'a, Access: Send + Clone> IndexedParallelIterator for DisjointSubsetsParIter<'a, Access> { + fn len(&self) -> usize { + self.subsets.len() + } + + fn drive>(self, consumer: C) -> >::Result { + bridge(self, consumer) + } + + fn with_producer>(self, callback: CB) -> CB::Output { + let num_subsets = self.subsets.len(); + callback.callback(DisjointSubsetsProducer { + access: self.access, + subsets: &self.subsets, + labels: self.labels, + range_start_idx: 0, + range_len: num_subsets, + }) + } +} + +struct DisjointSubsetsProducer<'a, Access> { + access: Access, + subsets: &'a NestedVec, + labels: &'a [usize], + // Range start/len represents the range represented by this producer + range_start_idx: usize, + range_len: usize, +} + +impl<'a, Access> Debug for DisjointSubsetsProducer<'a, Access> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("DisjointSubsetsProducer") + // .field("access", &"") + // .field("subsets", &self.subsets) + .field("range_start_idx", &self.range_start_idx) + .field("range_len", &self.range_len) + .finish() + } +} + +impl<'a, Access: Send + Clone> Producer for DisjointSubsetsProducer<'a, Access> { + type Item = SubsetAccess<'a, Access>; + type IntoIter = DisjointSubsetsIter<'a, Access>; + + fn into_iter(self) -> Self::IntoIter { + DisjointSubsetsIter { + access: self.access.clone(), + subsets: self.subsets, + labels: self.labels, + end: self.range_len + self.range_start_idx, + current_idx: self.range_start_idx, + } + } + + fn split_at(self, index: usize) -> (Self, Self) { + let producer_len = self.range_len; + assert!(index < producer_len); + let global_subset_idx = self.range_start_idx + index; + + let producer_left = DisjointSubsetsProducer { + access: self.access.clone(), + subsets: self.subsets, + labels: self.labels, + range_start_idx: self.range_start_idx, + range_len: index, + }; + + let producer_right = DisjointSubsetsProducer { + access: self.access, + subsets: self.subsets, + labels: self.labels, + range_start_idx: global_subset_idx, + range_len: producer_len - index, + }; + + (producer_left, producer_right) + } +} + +struct DisjointSubsetsIter<'a, Access> { + access: Access, + subsets: &'a NestedVec, + labels: &'a [usize], + // end is an index one-past the end of the iterator + end: usize, + // The current index that the iterator is at + current_idx: usize, +} + +impl<'a, Access> Debug for DisjointSubsetsIter<'a, Access> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("DisjointSubsetsIter") + // .field("access", &"") + // .field("subsets", &self.subsets) + .field("end", &self.end) + .field("current_idx", &self.current_idx) + .finish() + } +} + +impl<'a, Access: Clone> Iterator for DisjointSubsetsIter<'a, Access> { + type Item = SubsetAccess<'a, Access>; + + fn next(&mut self) -> Option { + if self.current_idx < self.end { + let access = SubsetAccess { + subset_label: *self.labels.get(self.current_idx).unwrap(), + global_indices: self.subsets.get(self.current_idx).unwrap(), + access: self.access.clone(), + }; + self.current_idx += 1; + Some(access) + } else { + None + } + } + + fn size_hint(&self) -> (usize, Option) { + let len = self.end - self.current_idx; + (len, Some(len)) + } +} + +impl<'a, Access: Clone> ExactSizeIterator for DisjointSubsetsIter<'a, Access> {} + +impl<'a, Access: Clone> DoubleEndedIterator for DisjointSubsetsIter<'a, Access> { + fn next_back(&mut self) -> Option { + if self.end > self.current_idx { + let subset_index = self.end - 1; + let access = SubsetAccess { + subset_label: *self.labels.get(subset_index).unwrap(), + global_indices: self.subsets.get(subset_index).unwrap(), + access: self.access.clone(), + }; + self.end -= 1; + Some(access) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use super::DisjointSubsets; + use super::DisjointSubsetsIter; + use super::ParallelStorage; + use nested_vec::NestedVec; + use proptest::collection::{btree_set, vec}; + use proptest::prelude::*; + use rand::rngs::StdRng; + use rand::seq::SliceRandom; + use rand::SeedableRng; + use rayon::iter::{IndexedParallelIterator, ParallelIterator}; + + #[test] + fn test_disjoint_subsets_iter() { + let subsets_vec = vec![vec![4, 5], vec![1, 2, 3], vec![6, 0]]; + let subset_labels = vec![0, 1, 2]; + let subsets = NestedVec::from(&subsets_vec); + + // Forward iteration only + { + // Range is over all subsets + let mut data = vec![10, 11, 12, 13, 14, 15, 16]; + let data_slice = data.as_mut_slice(); + + let access = data_slice.create_access(); + + let mut iter = DisjointSubsetsIter { + access, + subsets: &subsets, + labels: &subset_labels, + end: subsets.len(), + current_idx: 0, + }; + + assert_eq!(iter.len(), 3); + let subset_access = iter.next().unwrap(); + assert_eq!(subset_access.global_indices(), subsets_vec[0].as_slice()); + assert_eq!(iter.len(), 2); + let subset_access = iter.next().unwrap(); + assert_eq!(subset_access.global_indices(), subsets_vec[1].as_slice()); + assert_eq!(iter.len(), 1); + let subset_access = iter.next().unwrap(); + assert_eq!(subset_access.global_indices(), subsets_vec[2].as_slice()); + assert_eq!(iter.len(), 0); + assert!(iter.next().is_none()); + } + + // Forward iteration only + { + // Range is over subset + let mut data = vec![10, 11, 12, 13, 14, 15, 16]; + let data_slice = data.as_mut_slice(); + + let access = data_slice.create_access(); + + let mut iter = DisjointSubsetsIter { + access, + subsets: &subsets, + labels: &subset_labels, + end: subsets.len(), + current_idx: 1, + }; + + assert_eq!(iter.len(), 2); + let subset_access = iter.next().unwrap(); + assert_eq!(subset_access.global_indices(), subsets_vec[1].as_slice()); + assert_eq!(iter.len(), 1); + let subset_access = iter.next().unwrap(); + assert_eq!(subset_access.global_indices(), subsets_vec[2].as_slice()); + assert_eq!(iter.len(), 0); + assert!(iter.next().is_none()); + } + + // Backward iteration only + { + // Range is over subset + let mut data = vec![10, 11, 12, 13, 14, 15, 16]; + let data_slice = data.as_mut_slice(); + + let access = data_slice.create_access(); + + let mut iter = DisjointSubsetsIter { + access, + subsets: &subsets, + labels: &subset_labels, + end: subsets.len(), + current_idx: 0, + }; + + assert_eq!(iter.len(), 3); + let subset_access = iter.next_back().unwrap(); + assert_eq!(subset_access.global_indices(), subsets_vec[2].as_slice()); + assert_eq!(iter.len(), 2); + let subset_access = iter.next_back().unwrap(); + assert_eq!(subset_access.global_indices(), subsets_vec[1].as_slice()); + assert_eq!(iter.len(), 1); + let subset_access = iter.next_back().unwrap(); + assert_eq!(subset_access.global_indices(), subsets_vec[0].as_slice()); + assert_eq!(iter.len(), 0); + assert!(iter.next().is_none()); + } + + // Backward iteration only + { + // Range is over subset + let mut data = vec![10, 11, 12, 13, 14, 15, 16]; + let data_slice = data.as_mut_slice(); + + let access = data_slice.create_access(); + + let mut iter = DisjointSubsetsIter { + access, + subsets: &subsets, + labels: &subset_labels, + end: subsets.len() - 1, + current_idx: 0, + }; + + assert_eq!(iter.len(), 2); + let subset_access = iter.next_back().unwrap(); + assert_eq!(subset_access.global_indices(), subsets_vec[1].as_slice()); + assert_eq!(iter.len(), 1); + let subset_access = iter.next_back().unwrap(); + assert_eq!(subset_access.global_indices(), subsets_vec[0].as_slice()); + assert_eq!(iter.len(), 0); + assert!(iter.next().is_none()); + } + + // Forward and backward iteration + { + // Range is over subset + let mut data = vec![10, 11, 12, 13, 14, 15, 16]; + let data_slice = data.as_mut_slice(); + + let access = data_slice.create_access(); + + let mut iter = DisjointSubsetsIter { + access, + subsets: &subsets, + labels: &subset_labels, + end: subsets.len(), + current_idx: 0, + }; + + assert_eq!(iter.len(), 3); + let subset_access = iter.next().unwrap(); + assert_eq!(subset_access.global_indices(), subsets_vec[0].as_slice()); + assert_eq!(iter.len(), 2); + let subset_access = iter.next_back().unwrap(); + assert_eq!(subset_access.global_indices(), subsets_vec[2].as_slice()); + assert_eq!(iter.len(), 1); + let subset_access = iter.next().unwrap(); + assert_eq!(subset_access.global_indices(), subsets_vec[1].as_slice()); + assert_eq!(iter.len(), 0); + assert!(iter.next_back().is_none()); + assert!(iter.next().is_none()); + assert!(iter.next().is_none()); + assert!(iter.next_back().is_none()); + assert!(iter.next_back().is_none()); + assert!(iter.next().is_none()); + } + } + + #[test] + fn test_parallel() { + // TODO: Fixed seed + let mut rng = StdRng::seed_from_u64(458340234234); + + let mut unique_indices: Vec<_> = (0..100000).collect(); + unique_indices.shuffle(&mut rng); + + let chunks: Vec<_> = unique_indices + .chunks(10) + .map(|chunk| chunk.to_vec()) + .collect(); + + let labels = (0..chunks.len()).collect(); + + let disjoint_subsets = DisjointSubsets::try_from_disjoint_subsets(&chunks, labels).unwrap(); + + let mut output_par = vec![0; unique_indices.len()]; + disjoint_subsets + .subsets_par_iter(output_par.as_mut_slice()) + .zip_eq(&chunks) + // Try to ensure that rayon actually uses multiple threads, otherwise it might + // decide to run it all sequentially + .with_max_len(1) + .for_each(|(mut subset_access, chunk)| { + assert_eq!(subset_access.global_indices(), chunk.as_slice()); + for i in 0..chunk.len() { + *subset_access.get_mut(i) += 1; + } + }); + + let mut output_seq = vec![0; unique_indices.len()]; + chunks.iter().for_each(|chunk| { + for i in 0..chunk.len() { + output_seq[chunk[i]] += 1; + } + }); + + let expected_output = vec![1; unique_indices.len()]; + assert_eq!(output_seq, expected_output); + assert_eq!(output_par, expected_output); + } + + // TODO: Test the strategy itself! + // TODO: Our current strategy also enforces that the subsets have no duplicate indices, + // which is explicitly allowed by our algorithms, so we should include this too + fn disjoint_subsets_strategy() -> impl Strategy> { + let max_num_integers = 20usize; + (0..max_num_integers) + .prop_flat_map(|n| Just((0..n).collect::>())) + .prop_shuffle() + .prop_flat_map(|integers| { + let n = integers.len(); + let num_splits = 0..=n; + let split_indices = vec(0..n, num_splits); + (Just(integers), split_indices) + }) + .prop_map(|(integers, mut split_indices)| { + let mut subsets = Vec::with_capacity(split_indices.len() + 1); + split_indices.push(0); + split_indices.push(integers.len()); + split_indices.sort_unstable(); + for window in split_indices.windows(2) { + let idx = window[0]; + let idx_next = window[1]; + subsets.push(integers[idx..idx_next].to_vec()); + } + NestedVec::from(&subsets) + }) + } + + fn overlapping_subsets_strategy() -> impl Strategy> { + // Given a set of overlapping subsets, add the same index to multiple subsets, + // thereby ensuring that the subsets are no longer disjoint + let max_index = 20usize; + disjoint_subsets_strategy() + .prop_filter("Must have more than 1 subset", |subsets| subsets.len() > 1) + .prop_flat_map(move |subsets| { + let insertion_index = 0..max_index; + let subset_index_strategy = btree_set(0..subsets.len(), 2..=subsets.len()); + (Just(subsets), subset_index_strategy, insertion_index) + }) + .prop_map(|(subsets, subset_indices, insertion_index)| { + let mut subsets: Vec> = subsets.into(); + let num_subsets = subsets.len(); + for subset_idx in subset_indices { + subsets[subset_idx % num_subsets].push(insertion_index); + } + NestedVec::from(subsets) + }) + } + + proptest! { + #[test] + fn can_create_from_disjoint_subsets( + disjoint_subsets in disjoint_subsets_strategy() + ) { + let labels = (0 .. disjoint_subsets.len()).collect(); + let disjoint = DisjointSubsets::try_from_disjoint_subsets(disjoint_subsets, labels); + dbg!(&disjoint); + prop_assert!(disjoint.is_ok()); + } + + #[test] + fn refuses_to_create_from_overlapping_subsets( + subsets in overlapping_subsets_strategy() + ) { + let labels = (0 .. subsets.len()).collect(); + let disjoint = DisjointSubsets::try_from_disjoint_subsets(subsets, labels); + prop_assert!(disjoint.is_err()); + } + } +} diff --git a/paradis/src/slice.rs b/paradis/src/slice.rs new file mode 100644 index 0000000..1eb7787 --- /dev/null +++ b/paradis/src/slice.rs @@ -0,0 +1,53 @@ +use crate::{ParallelAccess, ParallelStorage}; +use std::marker::PhantomData; + +#[derive(Copy)] +pub struct ParallelSliceAccess<'a, T> { + ptr: *mut T, + marker: PhantomData<&'a mut T>, +} + +impl<'a, T> Clone for ParallelSliceAccess<'a, T> { + fn clone(&self) -> Self { + Self { + ptr: self.ptr, + marker: PhantomData, + } + } +} + +unsafe impl<'a, T: Sync> Sync for ParallelSliceAccess<'a, T> {} +unsafe impl<'a, T: Send> Send for ParallelSliceAccess<'a, T> {} + +unsafe impl<'a, 'b, T: 'b + Sync + Send> ParallelAccess<'b> for ParallelSliceAccess<'a, T> +where + 'a: 'b, +{ + type Record = &'b T; + type RecordMut = &'b mut T; + + unsafe fn get_unchecked(&self, global_index: usize) -> &T { + // TODO: This might technically be unsound. Should we use .wrapping_add, or something else? + &*self.ptr.add(global_index) + } + + unsafe fn get_unchecked_mut(&self, global_index: usize) -> &mut T { + // TODO: This might technically be unsound. Should we use .wrapping_add, or something else? + &mut *self.ptr.add(global_index) + } +} + +unsafe impl<'a, T: 'a + Sync + Send> ParallelStorage<'a> for [T] { + type Access = ParallelSliceAccess<'a, T>; + + fn create_access(&'a mut self) -> Self::Access { + ParallelSliceAccess { + ptr: self.as_mut_ptr(), + marker: PhantomData, + } + } + + fn len(&self) -> usize { + <[T]>::len(&self) + } +} diff --git a/paradis/test_sanitized.sh b/paradis/test_sanitized.sh new file mode 100755 index 0000000..4d22c3b --- /dev/null +++ b/paradis/test_sanitized.sh @@ -0,0 +1,8 @@ +#!/bin/sh +SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +export TSAN_OPTIONS="suppressions=$SCRIPTPATH/tsan_suppression.txt" +export RUST_TEST_THREADS=1 +export CARGO_INCREMENTAL=0 +export RUSTFLAGS="-Z sanitizer=thread" +export RAYON_NUM_THREADS=4 +cargo +nightly test -p paradis --target x86_64-unknown-linux-gnu --tests diff --git a/paradis/tsan_suppression.txt b/paradis/tsan_suppression.txt new file mode 100644 index 0000000..2adffa5 --- /dev/null +++ b/paradis/tsan_suppression.txt @@ -0,0 +1,3 @@ +race:core::sync::atomic::atomic +race:lazy_static::lazy::Lazy +race:alloc::sync::Arc diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..708eff4 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,5 @@ +max_width = 120 + +# Increasing max width to 120 causes many method chains to become single-line and hard to read, +# so we set the chain width to the equivalent default value for the standard max width setting +chain_width = 60 diff --git a/scene_runner/Cargo.toml b/scene_runner/Cargo.toml new file mode 100644 index 0000000..f253b61 --- /dev/null +++ b/scene_runner/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "scene_runner" +version = "0.1.0" +authors = ["Andreas Longva"] +edition = "2018" +build = "build.rs" +default-run = "dynamic_runner" +publish = false + +[dependencies] +simulation_toolbox = { path = "../simulation_toolbox" } +fenris = { path = "../fenris" } +hamilton = { path = "../hamilton" } +itertools = "0.9" +numeric_literals = "0.2" +ply-rs = "0.1.2" +structopt = "0.3" +once_cell = "1.3" +petgraph = { version = "0.5", default-features=false } +coarse-prof = "0.2" +log = "0.4" +fern = "0.6" +rand = "0.7" +serde = { version = "1.0", features = ["derive"] } +statrs = "0.12" +chrono = "0.4" +hostname = "0.3" +typetag = "0.1" +rayon = "1.3" +gnuplot = "0.0.35" +nalgebra-lapack= { version="0.13", default-features=false, features = ["intel-mkl"] } +mkl-corrode = { git = "https://github.com/Andlon/mkl-corrode.git", rev="0843a0b46234cd88d7a0e7489720514624207ad9", features = [ "openmp" ] } +global_stash = { path = "../global_stash" } + +[build-dependencies] +chrono = "0.4" +hostname = "0.3" +ignore = "0.4" diff --git a/scene_runner/build.rs b/scene_runner/build.rs new file mode 100644 index 0000000..f249126 --- /dev/null +++ b/scene_runner/build.rs @@ -0,0 +1,64 @@ +use std::error::Error; +use std::ffi::OsStr; +use std::fs; +use std::process::Command; + +use ignore::WalkBuilder; + +fn git_command_stdout, S: AsRef>(args: I) -> Result> { + Ok( + String::from_utf8_lossy(Command::new("git").args(args).output()?.stdout.as_slice()) + .trim_end_matches("\n") + .replace("\n", ";"), + ) +} + +fn main() -> Result<(), Box> { + // Prepare environment variables that will be baked into the binary + { + let last_commit = git_command_stdout(&["show", "-s", "--format=Commit: %H%nAuthor: %an, %aI%nTitle: '%s'"])?; + + let current_changes = git_command_stdout(&["status", "-b", "--porcelain"])?; + + let hostname = hostname::get()?.to_string_lossy().into_owned(); + let timestamp = chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false); + + println!("cargo:rustc-env=FEMPROTO2_BUILD_TIMESTAMP={}", timestamp); + println!("cargo:rustc-env=FEMPROTO2_BUILD_HOSTNAME={}", hostname); + println!("cargo:rustc-env=FEMPROTO2_GIT_LAST_COMMIT={}", last_commit); + println!("cargo:rustc-env=FEMPROTO2_GIT_CHANGES={}", current_changes); + } + + // List directories that should be watched by cargo for changes for a rebuild. + // Using the ignore crate allows to skip folders mentioned in .gitignore files, + // e.g. changes in the target or data output directory should not lead to a rebuild. + // + // The .git folder has to be treated separately as every git command (even read only commands) + // `touch` the .git folder. So we only want to rebuild on changes inside of the .git folder + // (to cause a rebuild on a local commit). + { + // Add all top-level files and folders in the ../.git directory to watch + for entry in fs::read_dir("../.git")? + // Skip access errors + .filter_map(|e| e.ok()) + // Skip symlinks + .filter(|e| e.metadata().is_ok()) + { + println!("cargo:rerun-if-changed={}", entry.path().to_str().unwrap()); + } + + // Add all other folders recursively from ../ that are not excluded per .gitignore etc. + for entry in WalkBuilder::new("../") + .add_custom_ignore_filename(".git") + .follow_links(false) + .build() + .filter_map(|e| e.ok()) + { + if entry.path().is_dir() { + println!("cargo:rerun-if-changed={}", entry.path().to_str().unwrap()); + } + } + } + + Ok(()) +} diff --git a/scene_runner/src/bin/dynamic_runner.rs b/scene_runner/src/bin/dynamic_runner.rs new file mode 100644 index 0000000..77ce26a --- /dev/null +++ b/scene_runner/src/bin/dynamic_runner.rs @@ -0,0 +1,653 @@ +use std::cmp::min; +use std::env; +use std::error::Error; +use std::fmt; +use std::fmt::Display; +use std::fs; +use std::io::Write; +use std::path::PathBuf; +use std::time::Instant; + +use coarse_prof::profile; +use global_stash::stash_scope; +use log::{error, info, warn}; +use structopt::StructOpt; + +use hamilton::{register_component, Component, FilterSystem, StorageContainer, System, SystemCollection, Systems}; +use simulation_toolbox::components::{ + get_export_sequence_index, get_gravity, get_step_index, get_time_step, set_gravity, ExportSequenceIndex, Gravity, + SimulationTime, StepIndex, TimeStep, TimingSystem, +}; +use simulation_toolbox::io::json_helper; +use simulation_toolbox::io::ply::{ + PlyFem2dOutput, PlyInterpolatedPoints2dOutput, PlyPolyMesh2dOutput, PlyPolyMesh3dOutput, PlyVolumeMesh2dOutput, +}; +use simulation_toolbox::io::vtk::{VtkFemOutput, VtkSurfaceMeshOutput, VtkVolumeMeshOutput}; + +use scene_runner::scenes::{available_scenes, load_scene, Scene, SceneParameters}; + +static BUILD_TIMESTAMP: Option<&'static str> = option_env!("FEMPROTO2_BUILD_TIMESTAMP"); +static BUILD_HOSTNAME: Option<&'static str> = option_env!("FEMPROTO2_BUILD_HOSTNAME"); +static GIT_LAST_COMMIT: Option<&'static str> = option_env!("FEMPROTO2_GIT_LAST_COMMIT"); +static GIT_CHANGES: Option<&'static str> = option_env!("FEMPROTO2_GIT_CHANGES"); + +#[derive(Debug, StructOpt)] +struct CommandlineArgs { + #[structopt( + short = "-s", + long = "--scene", + help = "Name of scene to simulate", + required_unless = "list-scenes" + )] + scene: Option, + #[structopt(short = "-l", long = "--list-scenes", help = "List available scenes")] + list_scenes: bool, + #[structopt(long, help = "Enable PLY mesh data output")] + output_ply: bool, + #[structopt(long, help = "Disable VTK mesh data output")] + no_output_vtk: bool, + #[structopt( + long, + default_value = "data", + parse(from_os_str), + help = "Base directory for output files" + )] + output_dir: PathBuf, + #[structopt( + long, + default_value = "assets", + parse(from_os_str), + help = "Base directory for asset files" + )] + asset_dir: PathBuf, + #[structopt(long = "--output-fps", help = "Override output systems to output at a fixed fps")] + output_fps: Option, + #[structopt(long, help = "Overrides duration of scene")] + duration: Option, + #[structopt(long = "--dt", help = "Overrides time step")] + timestep: Option, + #[structopt( + long, + help = "Enables printing solver timings at every file export (with export FPS)" + )] + print_timings_output: bool, + #[structopt(long, parse(from_os_str), help = "Configuration file for the scene")] + config_file: Option, + #[structopt( + long, + parse(from_os_str), + help = "Path for the logfile relative to 'output-dir/scene-name'" + )] + log_file: Option, + #[structopt( + long, + default_value = "0", + help = "The number of threads to use for the rayon thread pool, if not specified it will be read from env or default rayon value" + )] + num_threads: usize, + #[structopt( + long, + help = "Enables writing out the global statistics stash to file on every timestep" + )] + dump_stash: bool, +} + +fn main() -> Result<(), Box> { + let args = CommandlineArgs::from_args(); + if args.list_scenes { + list_scenes()?; + } else { + initialize_logging(&args)?; + initialize_thread_pool(&args)?; + + info!("Started dynamic_runner"); + print_git_info()?; + + info!("Running on'{}'", hostname::get()?.to_string_lossy()); + info!("Executable path: '{}'", env::current_exe()?.to_string_lossy()); + info!("Working directory: '{}'", env::current_dir()?.to_string_lossy()); + info!("Full command line: '{}'", env::args().collect::>().join(" ")); + + if let Err(err) = run_scene(&args) { + error!("Scene returned error: {}", err); + error!("Aborting."); + } else { + info!("Exiting."); + } + } + + Ok(()) +} + +fn list_scenes() -> Result<(), Box> { + println!("Available scenes: "); + for scene in available_scenes() { + println!(" - {}", scene); + } + Ok(()) +} + +fn run_scene(args: &CommandlineArgs) -> Result<(), Box> { + let scene_name = args.scene.as_ref().unwrap(); + + // Try to load JSON config file + let config_file = if let Some(config_path) = &args.config_file { + let json = json_helper::parse_json_from_path(config_path)? + .as_object() + .cloned() + .ok_or_else(|| { + format!( + "Expected a JSON object on the highest level in config file {}", + config_path.to_string_lossy() + ) + })?; + + // Try to get section corresponding to the selected scene + let scene_config = json.get(scene_name).ok_or_else(|| { + format!( + "Did not find entry for scene `{}` in config file `{}`", + scene_name, + config_path.to_string_lossy() + ) + })?; + + Some(json_helper::JsonWrapper::new(scene_config.clone())) + } else { + None + }; + + // Build parameters to pass to scene constructor + let scene_params = SceneParameters { + output_dir: args.output_dir.join(scene_name).clone(), + asset_dir: args.asset_dir.clone(), + config_file, + }; + + info!("Starting to load scene {}.", scene_name); + let mut scene = load_scene(&scene_name, &scene_params)?; + + assert_eq!(&scene.name, scene_name); + info!("Loaded scene {}.", scene_name); + + if let Some(duration) = &args.duration { + info!( + "Overriding duration set by scene. Original: {}. New: {}.", + scene.duration, *duration + ); + scene.duration = *duration; + } + + simulate_scene(scene, &args)?; + + coarse_prof_write_string()? + .split("\n") + .for_each(|l| info!("{}", l)); + info!("Simulation finished."); + + Ok(()) +} + +fn initialize_logging(args: &CommandlineArgs) -> Result<(), Box> { + // Try to load log filter level from env + let mut unknown_log_filter_level = None; + let log_filter_level = if let Some(log_level) = std::env::var_os("RUST_LOG") { + let log_level = log_level.to_string_lossy().to_ascii_lowercase(); + match log_level.as_str() { + "off" => log::LevelFilter::Off, + "error" => log::LevelFilter::Error, + "warn" => log::LevelFilter::Warn, + "info" => log::LevelFilter::Info, + "debug" => log::LevelFilter::Debug, + "trace" => log::LevelFilter::Trace, + _ => { + unknown_log_filter_level = Some(log_level); + log::LevelFilter::Info + } + } + } else { + // Default log level + log::LevelFilter::Info + }; + + let log_dir = if let Some(scene_name) = &args.scene { + args.output_dir.join(scene_name) + } else { + args.output_dir.clone() + }; + fs::create_dir_all(&log_dir).map_err(|e| { + format!( + "Unable to create output directory '{}' ({:?})", + log_dir.to_string_lossy(), + e + ) + })?; + + let log_file_path = if let Some(log_file_name) = &args.log_file { + log_dir.join(log_file_name) + } else { + log_dir.join(format!( + "femproto2_{}.log", + chrono::Local::now().format("%F_%H-%M-%S-%6f") + )) + }; + + let log_file = fs::OpenOptions::new() + .write(true) + .create(true) + // TODO: To append or to truncate? + .truncate(true) + .append(false) + .open(&log_file_path) + .map_err(|e| { + format!( + "Unable to open log file '{}' for writing ({:?})", + log_file_path.to_string_lossy(), + e + ) + })?; + + fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "[{}][{}][{}] {}", + chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Micros, false), + record.target(), + record.level(), + message + )) + }) + .level(log_filter_level) + .chain(std::io::stdout()) + .chain(log_file) + .apply() + .map_err(|e| format!("Unable to apply logger configuration ({:?})", e))?; + + if let Some(filter_level) = unknown_log_filter_level { + error!( + "Unkown log filter level '{}' defined in 'RUST_LOG' env variable, using INFO instead.", + filter_level + ); + } + + Ok(()) +} + +fn initialize_thread_pool(args: &CommandlineArgs) -> Result<(), Box> { + rayon::ThreadPoolBuilder::new() + .num_threads(args.num_threads) + .build_global()?; + // TODO: Is there truly no way to determine the *actual* number of threads + // in use by the global thread pool? Though, to be fair, it might be that a future version + // of Rayon might have variable number of threads in the pool, so I suppose we are in + // any case interested in the *maximum* number of threads in the pool. + if args.num_threads == 0usize { + if let Ok(rayon_num_threads) = env::var("RAYON_NUM_THREADS") { + info!("Number of Rayon threads: {} (from environment)", rayon_num_threads); + } else { + info!("Number of Rayon threads: default"); + } + } else { + info!("Number of Rayon threads: {} (from command-line args)", args.num_threads); + }; + + Ok(()) +} + +fn print_git_info() -> Result<(), Box> { + info!("Build info"); + + let build_timestamp = BUILD_TIMESTAMP.unwrap_or("Unknown"); + let build_hostname = BUILD_HOSTNAME.unwrap_or("Unknown"); + + info!("\tBuild timestamp: {}", build_timestamp); + info!("\tBuild hostname: '{}'", build_hostname); + + if let Some(last_commit) = GIT_LAST_COMMIT { + info!("\tCommand 'git show -s --format=Commit: %H%nAuthor: %an, %aI%nTitle: '%s'':"); + for line in last_commit.split(";") { + info!("\t\t{}", line); + } + } else { + warn!("\tGit commit information was unavailable at build time"); + } + + if let Some(git_changes) = GIT_CHANGES { + info!("\tCommand: 'git status -b --porcelain':"); + for line in git_changes.split(";") { + info!("\t\t{}", line); + } + } else { + warn!("\tGit file status information was unavailable at build time"); + } + + Ok(()) +} + +fn get_simulation_time(state: &StorageContainer) -> f64 { + simulation_toolbox::components::get_simulation_time(state) + .expect("SimulationTime must always be a component in the state.") +} + +fn set_simulation_time(state: &mut StorageContainer, new_time: impl Into) { + state.replace_storage(::Storage::new(new_time.into())); +} + +fn increment_step_index(state: &mut StorageContainer) { + let step_index = state + .try_get_component_storage::() + .expect("Simulation must have StepIndex component.") + .borrow() + .get_component() + .clone(); + let new_step_index = StepIndex(step_index.0 + 1); + state.replace_storage(::Storage::new(new_step_index)); +} + +fn increment_export_sequence_index(state: &mut StorageContainer) -> bool { + let current_simulation_time = get_simulation_time(state); + let current_dt = get_time_step(state).expect("Simulation must have TimeStep component."); + + let ExportSequenceIndex { + index, + prev_export_time, + export_interval, + } = state + .try_get_component_storage::() + .expect("Simulation must have ExportSequenceIndex component.") + .borrow() + .get_component() + .clone(); + + // TODO: Explicitly compute the StepIndex such that no StepIndex is closer for incrementing the ExportSequenceIndex + + // Elapsed simulation time since last export + let elapsed = prev_export_time.map(|prev_export_time| current_simulation_time - prev_export_time); + // Distance to reaching an export interval mark + let delta_export = elapsed.map(|elapsed| (export_interval - elapsed).abs()); + // Check if distance is small enough to make an export + let do_export = if let Some(delta_export) = delta_export { + (elapsed.unwrap() > export_interval) + || (delta_export < 100.0 * std::f64::EPSILON) + || (delta_export < 0.01 * current_dt) + } else { + true + }; + + if do_export { + let new_sequence_index = ExportSequenceIndex { + index: index + 1, + prev_export_time: Some(current_simulation_time), + export_interval, + }; + state.replace_storage(::Storage::new(new_sequence_index)); + } + + do_export +} + +pub fn new_output_sequence_filter_system(system: S) -> impl System +where + S: System, +{ + FilterSystem { + predicate: { + let mut last_export_index = None; + move |state| { + let current_export_index = get_export_sequence_index(state)?; + if last_export_index.is_none() || current_export_index > last_export_index.unwrap() { + last_export_index = Some(current_export_index); + return Ok(true); + } + Ok(false) + } + }, + system, + } +} + +#[derive(Debug)] +struct DumpGlobalStashSystem { + output_dir: PathBuf, +} + +impl DumpGlobalStashSystem { + pub fn new>(output_dir: P) -> Self { + Self { + output_dir: output_dir.into(), + } + } +} + +impl Display for DumpGlobalStashSystem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "GlobalStashDumper") + } +} + +impl System for DumpGlobalStashSystem { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + let step_index = get_step_index(data)?; + + let stash_json = global_stash::to_string_pretty()?; + fs::create_dir_all(&self.output_dir)?; + let mut json_file = fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open( + self.output_dir + .join(format!("global_stash_{}.json", step_index)), + )?; + json_file.write_all(stash_json.as_bytes())?; + + global_stash::clear_values(); + Ok(()) + } +} + +fn register_analysis_systems(systems: &mut Systems, args: &CommandlineArgs, scene_name: &str) { + let output_dir = &args.output_dir; + let vtk_basepath = output_dir.join(format!("{}/vtk/", scene_name)); + let ply_basepath = output_dir.join(format!("{}/ply/", scene_name)); + + if !args.no_output_vtk { + let vtk_systems = SystemCollection(vec![ + Box::new(VtkVolumeMeshOutput { + base_path: vtk_basepath.clone(), + }), + Box::new(VtkSurfaceMeshOutput { + base_path: vtk_basepath.clone(), + }), + Box::new(VtkFemOutput { + base_path: vtk_basepath.clone(), + }), + ]); + systems.add_system(Box::new(new_output_sequence_filter_system(TimingSystem::new( + "VTK output", + vtk_systems, + )))); + } + + if args.output_ply { + let ply_systems = SystemCollection(vec![ + Box::new(PlyFem2dOutput { + base_path: ply_basepath.clone(), + }), + Box::new(PlyVolumeMesh2dOutput { + base_path: ply_basepath.clone(), + }), + Box::new(PlyInterpolatedPoints2dOutput { + base_path: ply_basepath.clone(), + }), + Box::new(PlyPolyMesh3dOutput { + base_path: ply_basepath.clone(), + }), + Box::new(PlyPolyMesh2dOutput { + base_path: ply_basepath.clone(), + }), + ]); + systems.add_system(Box::new(new_output_sequence_filter_system(TimingSystem::new( + "PLY output", + ply_systems, + )))); + } + + if args.dump_stash { + systems.add_system(Box::new(DumpGlobalStashSystem::new( + output_dir.join(format!("{}/stash/", scene_name)), + ))); + } +} + +fn simulate_scene(scene: Scene, args: &CommandlineArgs) -> Result<(), Box> { + register_component::()?; + register_component::()?; + register_component::()?; + register_component::()?; + + let mut simulation_systems = scene.simulation_systems; + let mut analysis_systems = scene.analysis_systems; + let mut state = scene.initial_state; + let duration = scene.duration; + + if let Some(dt) = args.timestep { + state.replace_storage(::Storage::new(TimeStep(dt))); + } + + let dt: f64 = if let Some(timestep) = state.try_get_component_storage::() { + timestep.borrow().get_component().clone().into() + } else { + let dt = 1.0 / 60.0; + state.replace_storage(::Storage::new(TimeStep(dt))); + dt + }; + + if state.try_get_component_storage::().is_none() { + // Set a default time step if the scene itself does not set one + state.replace_storage(::Storage::new(TimeStep(1.0 / 60.0))); + } + + if state.try_get_component_storage::().is_none() { + // If we have no StepIndex yet, start at 0 + state.replace_storage(::Storage::new(StepIndex(0))); + } + + // Initialize the export sequence index + { + let export_interval = if let Some(fps) = args.output_fps { + // Use frame time as export interval + fps.recip() + } else { + 0.0 + }; + + state.replace_storage(::Storage::new(ExportSequenceIndex { + index: 0, + prev_export_time: None, + export_interval, + })); + } + + // Reset simulation time + set_simulation_time(&mut state, 0.0); + // Set gravity only if not set by the scene + if !get_gravity(&mut state).is_ok() { + set_gravity(&mut state, -9.81); + } + + register_analysis_systems(&mut analysis_systems, &args, &scene.name); + + // Run analysis systems for initial state. Ensures output to VTK/PLY for initial state + analysis_systems.run_all(&state)?; + + let mut progress_printer = ProgressPrinter::with_num_reports(100); + + info!("Starting simulation..."); + + { + profile!("simulation"); + stash_scope!("simulation"); + while get_simulation_time(&state) < duration { + let do_export = { + profile!("timestep"); + stash_scope!("timestep"); + increment_step_index(&mut state); + let do_export = increment_export_sequence_index(&mut state); + + let step_index = state + .try_get_component_storage::() + .unwrap() + .borrow() + .get_component() + .0; + let t_before_sim = Instant::now(); + info!( + "Starting simulation of step {} @ time {:.8}s...", + step_index, + get_simulation_time(&state) + ); + + simulation_systems.run_all(&state)?; + + let new_time = get_simulation_time(&state) + dt; + set_simulation_time(&mut state, new_time); + analysis_systems.run_all(&state)?; + + // Print simulation timing + let elapsed = t_before_sim.elapsed(); + let elapsed_s = (elapsed.as_secs() as f64) + (elapsed.subsec_nanos() as f64) * 1e-9; + info!("Measured time for simulation of step {}: {:.6}s", step_index, elapsed_s); + + progress_printer.progress(new_time / duration); + + do_export + }; + + if args.print_timings_output && do_export && get_simulation_time(&state) < duration { + coarse_prof_write_string()? + .split("\n") + .for_each(|l| info!("{}", l)); + } + } + } + + progress_printer.progress(1.0); + + Ok(()) +} + +struct ProgressPrinter { + // Total number of reports to make + num_reports: usize, + // Previously reported progress, measured as integers in the interval [0, num_reports). + current_progress: usize, +} + +impl ProgressPrinter { + fn with_num_reports(num_reports: usize) -> Self { + Self { + num_reports, + current_progress: 0, + } + } + + fn progress(&mut self, progress_fraction: f64) { + let quantizized = (progress_fraction * self.num_reports as f64).floor() as usize; + // Possible off-by-one errors here, but whatever + let quantitized = min(quantizized, self.num_reports); + if quantitized > self.current_progress { + self.current_progress = quantitized; + info!( + "Current progress: {} %.", + (100.0 * self.current_progress as f64 / self.num_reports as f64) + ) + } + } +} + +/// Returns the coarse_prof write output as a string +fn coarse_prof_write_string() -> Result> { + let mut buffer = Vec::new(); + coarse_prof::write(&mut buffer)?; + Ok(String::from_utf8_lossy(buffer.as_slice()).into_owned()) +} diff --git a/scene_runner/src/lib.rs b/scene_runner/src/lib.rs new file mode 100644 index 0000000..002ad5b --- /dev/null +++ b/scene_runner/src/lib.rs @@ -0,0 +1,3 @@ +#[allow(dead_code)] +pub(crate) mod meshes; +pub mod scenes; diff --git a/scene_runner/src/meshes.rs b/scene_runner/src/meshes.rs new file mode 100644 index 0000000..4a03681 --- /dev/null +++ b/scene_runner/src/meshes.rs @@ -0,0 +1,77 @@ +use std::error::Error; +use std::fs::OpenOptions; +use std::io::{BufReader, Read}; +use std::path::Path; + +use fenris::connectivity::Connectivity; +use fenris::mesh::{Mesh, TriangleMesh2d}; +use fenris::nalgebra::allocator::Allocator; +use fenris::nalgebra::{DefaultAllocator, DimName, Point}; +use once_cell::sync::Lazy; +use simulation_toolbox::io::msh::{try_mesh_from_bytes, TryConnectivityFromMshElement, TryVertexFromMshNode}; + +fn read_bytes

(path: P) -> Result, Box> +where + P: AsRef, +{ + let file = OpenOptions::new() + .read(true) + .write(false) + .create(false) + .open(path)?; + let mut buf_reader = BufReader::new(file); + + let mut data = Vec::new(); + buf_reader.read_to_end(&mut data)?; + Ok(data) +} + +fn load_mesh_from_file_internal(asset_dir: P, file_name: &str) -> Result, Box> +where + P: AsRef, + D: DimName, + C: Connectivity + TryConnectivityFromMshElement, + Point: TryVertexFromMshNode, + DefaultAllocator: Allocator, +{ + let file_path = asset_dir.as_ref().join(file_name); + let msh_bytes = read_bytes(file_path)?; + try_mesh_from_bytes(&msh_bytes) +} + +pub fn load_mesh_from_file(asset_dir: P, file_name: &str) -> Result, Box> +where + P: AsRef, + D: DimName, + C: Connectivity + TryConnectivityFromMshElement, + Point: TryVertexFromMshNode, + DefaultAllocator: Allocator, +{ + load_mesh_from_file_internal(asset_dir, file_name) + .map_err(|e| Box::from(format!("Error occured during mesh loading of `{}`: {}", file_name, e))) +} + +pub(crate) static BIKE_TRI2D_MESH_COARSE: Lazy> = Lazy::new(|| { + load_mesh_from_file("assets", "meshes/bike_coarse.obj_linear.msh").expect("Unable to load 'BIKE_MSH_COARSE'") +}); + +pub(crate) static BIKE_TRI2D_MESH_FINE: Lazy> = Lazy::new(|| { + load_mesh_from_file("assets", "meshes/bike_fine.obj_linear.msh").expect("Unable to load 'BIKE_MSH_FINE'") +}); + +pub(crate) static ELEPHANT_TRI2D_MESH_COARSE: Lazy> = Lazy::new(|| { + load_mesh_from_file("assets", "meshes/elephant_base.obj_coarse.msh").expect("Unable to load 'ELEPHANT_MSH_COARSE'") +}); + +pub(crate) static ELEPHANT_TRI2D_MESH_FINE: Lazy> = Lazy::new(|| { + load_mesh_from_file("assets", "meshes/elephant_base.obj_fine.msh").expect("Unable to load 'ELEPHANT_MSH_FINE'") +}); + +pub(crate) static ELEPHANT_TRI2D_MESH_SUPER_FINE: Lazy> = Lazy::new(|| { + load_mesh_from_file("assets", "meshes/elephant_base.obj_super_fine.msh") + .expect("Unable to load 'ELEPHANT_MSH_SUPER_FINE'") +}); + +pub(crate) static ELEPHANT_CAGE_TRI2D_MESH_FINE: Lazy> = Lazy::new(|| { + load_mesh_from_file("assets", "meshes/elephant_cage.obj_coarse.msh").expect("Unable to load 'ELEPHANT_MSH_CAGE'") +}); diff --git a/scene_runner/src/scenes/armadillo_slingshot.rs b/scene_runner/src/scenes/armadillo_slingshot.rs new file mode 100644 index 0000000..8f70fdd --- /dev/null +++ b/scene_runner/src/scenes/armadillo_slingshot.rs @@ -0,0 +1,581 @@ +use std::error::Error; + +use crate::scenes::{filtered_vertex_indices, Scene, SceneConstructor, SceneParameters}; + +use crate::meshes::load_mesh_from_file; +use crate::scenes::helpers::BodyInitializer3d; +use core::fmt; +use fenris::connectivity::{Connectivity, ConnectivityMut}; +use fenris::embedding::{ + embed_mesh_3d, embed_quadrature_3d, embed_quadrature_3d_with_opts, EmbeddedModelBuilder, QuadratureOptions, + StabilizationOptions, +}; +use fenris::geometry::polymesh::PolyMesh3d; +use fenris::geometry::{ConvexPolygon3d, Quad3d}; +use fenris::lp_solvers::GlopSolver; +use fenris::mesh::{Mesh3d, Tet10Mesh, Tet4Mesh}; +use fenris::model::NodalModel; +use fenris::nalgebra::{Point3, Vector3}; +use fenris::nested_vec::NestedVec; +use fenris::quadrature::{ + tet_quadrature_strength_1, tet_quadrature_strength_2, tet_quadrature_strength_3, tet_quadrature_strength_5, +}; +use fenris::reorder::reorder_mesh_par; +use fenris::rtree::GeometryCollectionAccelerator; +use fenris::solid::materials::{StableNeoHookeanMaterial, YoungPoisson}; +use fenris::space::FiniteElementSpace; +use hamilton::{Component, Entity, StorageContainer, System, Systems}; +use log::info; +use simulation_toolbox::components::{ + get_simulation_time, set_gravity, PolyMesh3dCollection, PolyMesh3dComponent, TimeStep, +}; +use simulation_toolbox::fem::bcs::{ConstantUniformDisplacement, ConstantUniformVelocity, Union}; +use simulation_toolbox::io::obj::load_single_surface_polymesh3d_obj; +use simulation_toolbox::io::ply::dump_polymesh_faces_ply; +use simulation_toolbox::match_on_finite_element_model_3d; + +#[allow(unused)] +use simulation_toolbox::fem::{FiniteElementIntegrator, IntegratorSettings}; + +use simulation_toolbox::fem::newton_cg::NewtonCgIntegrator3d; +use simulation_toolbox::fem::schwarz_precond::SchwarzPreconditionerComponent; +use simulation_toolbox::fem::{ + DirichletBoundaryConditionComponent, DirichletBoundaryConditions, FiniteElementMeshDeformer, FiniteElementModel3d, + Material, +}; + +pub fn scenes() -> Vec { + vec![ + SceneConstructor { + name: "armadillo_slingshot_fem_tet4_500".to_string(), + constructor: armadillo_slingshot_fem_tet4_500, + }, + SceneConstructor { + name: "armadillo_slingshot_fem_tet4_1500".to_string(), + constructor: armadillo_slingshot_fem_tet4_1500, + }, + SceneConstructor { + name: "armadillo_slingshot_fem_tet4_3000".to_string(), + constructor: armadillo_slingshot_fem_tet4_3000, + }, + SceneConstructor { + name: "armadillo_slingshot_fem_tet4_5000".to_string(), + constructor: armadillo_slingshot_fem_tet4_5000, + }, + SceneConstructor { + name: "armadillo_slingshot_fem_tet4_full".to_string(), + constructor: armadillo_slingshot_fem_tet4_full, + }, + SceneConstructor { + name: "armadillo_slingshot_fem_tet10_500".to_string(), + constructor: armadillo_slingshot_fem_tet10_500, + }, + SceneConstructor { + name: "armadillo_slingshot_fem_tet10_1000".to_string(), + constructor: armadillo_slingshot_fem_tet10_1000, + }, + SceneConstructor { + name: "armadillo_slingshot_fem_tet10_3000".to_string(), + constructor: armadillo_slingshot_fem_tet10_3000, + }, + SceneConstructor { + name: "armadillo_slingshot_fem_tet10_5000".to_string(), + constructor: armadillo_slingshot_fem_tet10_5000, + }, + SceneConstructor { + name: "armadillo_slingshot_embedded_tet4_500".to_string(), + constructor: armadillo_slingshot_embedded_tet4_500, + }, + SceneConstructor { + name: "armadillo_slingshot_embedded_tet4_1500".to_string(), + constructor: armadillo_slingshot_embedded_tet4_1500, + }, + SceneConstructor { + name: "armadillo_slingshot_embedded_tet4_3000".to_string(), + constructor: armadillo_slingshot_embedded_tet4_3000, + }, + SceneConstructor { + name: "armadillo_slingshot_embedded_tet4_5000".to_string(), + constructor: armadillo_slingshot_embedded_tet4_5000, + }, + SceneConstructor { + name: "armadillo_slingshot_embedded_tet10_500".to_string(), + constructor: armadillo_slingshot_embedded_tet10_500, + }, + SceneConstructor { + name: "armadillo_slingshot_embedded_tet10_1000".to_string(), + constructor: armadillo_slingshot_embedded_tet10_1000, + }, + SceneConstructor { + name: "armadillo_slingshot_embedded_tet10_1500".to_string(), + constructor: armadillo_slingshot_embedded_tet10_1500, + }, + SceneConstructor { + name: "armadillo_slingshot_embedded_tet10_3000".to_string(), + constructor: armadillo_slingshot_embedded_tet10_3000, + }, + SceneConstructor { + name: "armadillo_slingshot_embedded_tet10_5000".to_string(), + constructor: armadillo_slingshot_embedded_tet10_5000, + }, + ] +} + +fn initial_scene(name: &str) -> Scene { + // Use y-axis gravity to simplify working with meshes that are oriented along the y-axis + let mut initial_state = Default::default(); + set_gravity(&mut initial_state, Vector3::new(0.0, -9.81, 0.0)); + let dt = 2e-3; + initial_state.replace_storage(::Storage::new(TimeStep(dt))); + Scene { + initial_state, + simulation_systems: Default::default(), + analysis_systems: Default::default(), + duration: 10.0, + name: String::from(name), + } +} + +fn default_material() -> Material { + Material { + density: 1000.0, + mass_damping_coefficient: None, + stiffness_damping_coefficient: Some(0.015), + elastic_model: StableNeoHookeanMaterial::from(YoungPoisson { + young: 5e5, + poisson: 0.40, + }) + .into(), + } +} + +fn add_systems(systems: &mut Systems) { + //let integrator_settings = IntegratorSettings::default().set_project_stiffness(false); + //systems.add_system(Box::new(FiniteElementIntegrator::with_settings(integrator_settings))); + systems.add_system(Box::new(NewtonCgIntegrator3d::default())); + systems.add_system(Box::new(FiniteElementMeshDeformer)); +} + +fn load_mesh(params: &SceneParameters, filename: &str) -> Result, Box> { + load_mesh_from_file(¶ms.asset_dir, &format!("meshes/armadillo_slingshot/{}", filename)) +} + +fn load_fine_embedded_mesh(params: &SceneParameters) -> Result, Box> { + load_mesh(params, "armadillo_slingshot.msh") +} + +/// System that switches out BCs after a given simulation time +#[derive(Debug)] +pub struct BoundaryConditionSwitchSystem { + time_for_switch: f64, + new_bcs: Option>, + entity: Entity, +} + +impl fmt::Display for BoundaryConditionSwitchSystem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "BoundaryConditionSwitchSystem") + } +} + +impl System for BoundaryConditionSwitchSystem { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + let t = get_simulation_time(data); + let mut bcs = data + .get_component_storage::() + .borrow_mut(); + + if let Ok(t) = t { + if t >= self.time_for_switch { + if let Some(new_bcs) = self.new_bcs.take() { + let bc_component = bcs + .get_component_mut(self.entity) + .expect("Entity must be in simulation"); + bc_component.bc = new_bcs; + } + } + } + + Ok(()) + } +} + +struct BoundaryConditions { + // BCs at the beginning of the scene + initial_bc: Box, + // BCs after switch + final_bc: Box, + // Time when switch takes place + switch_time: f64, +} + +fn set_up_boundary_conditions(mesh_vertices: &[Point3]) -> BoundaryConditions { + // Epsilon for classifying nodes as belonging to a constrained surface + let dist_eps = 0.005; + + let left_arm_quad = Quad3d::from_vertices([ + Point3::new(-1.3292, 1.18282, -0.500656), + Point3::new(-1.17938, 1.08153, -0.221283), + Point3::new(-1.14583, 1.29737, -0.16103), + Point3::new(-1.29661, 1.39865, -0.439886), + ]); + + let left_arm_indices = + filtered_vertex_indices(mesh_vertices, |v| left_arm_quad.project_point(v).distance <= dist_eps); + + let right_arm_quad = Quad3d::from_vertices([ + Point3::new(1.18738, 1.11562, -0.0642211), + Point3::new(1.29702, 1.26411, -0.28097), + Point3::new(1.25493, 1.41509, -0.194934), + Point3::new(1.14410, 1.26502, 0.0177633), + ]); + + let right_arm_indices = + filtered_vertex_indices(mesh_vertices, |v| right_arm_quad.project_point(v).distance <= dist_eps); + + let back_plate_quad = Quad3d::from_vertices([ + Point3::new(-0.131145, 0.405836, 0.711431), + Point3::new(0.157981, 0.405836, 0.711431), + Point3::new(0.157981, 0.705468, 0.711431), + Point3::new(-0.130775, 0.705468, 0.711431), + ]); + let back_plate_indices = + filtered_vertex_indices(mesh_vertices, |v| back_plate_quad.project_point(v).distance <= dist_eps); + + let feet_nodes = filtered_vertex_indices(mesh_vertices, |v| v.y <= -1.06); + + let mut static_nodes = Vec::new(); + static_nodes.extend(left_arm_indices); + static_nodes.extend(right_arm_indices); + static_nodes.extend(feet_nodes); + + let static_bc = ConstantUniformDisplacement::new(&static_nodes, Vector3::zeros()); + let slingshot_bc = ConstantUniformVelocity::new(&back_plate_indices, Vector3::new(0.0, 0.0, 0.5)); + + BoundaryConditions { + initial_bc: Box::new( + Union::try_new(vec![Box::new(static_bc.clone()), Box::new(slingshot_bc.clone())]).unwrap(), + ), + final_bc: Box::new(static_bc), + switch_time: 2.5, + } +} + +fn add_model<'a, Model, C>( + params: &SceneParameters, + scene: &mut Scene, + model: Model, + mesh: &Mesh3d, + _volume_poly_mesh: PolyMesh3d, +) -> Result> +where + Model: Into, + C: Connectivity, + C::FaceConnectivity: ConnectivityMut, +{ + let model = model.into(); + + match_on_finite_element_model_3d!(model, model => { + info!("Setting up model. Vertices: {}. Elements: {}", + model.vertices().len(), model.num_connectivities()); + }); + + info!("Generating render mesh"); + let render_mesh_path = params + .asset_dir + .join("meshes/armadillo_slingshot/armadillo_slingshot_render.obj"); + let render_surface_mesh = load_single_surface_polymesh3d_obj(&render_mesh_path)?; + + info!("Generating wireframes"); + let (wireframe_volume, wireframe_surface) = { + let fe_mesh_volume = mesh.extract_face_soup(); + let fe_mesh_surface = mesh.extract_surface_mesh(); + let mut wireframe_volume = PolyMesh3d::from_surface_mesh(&fe_mesh_volume); + let mut wireframe_surface = PolyMesh3d::from_surface_mesh(&fe_mesh_surface); + wireframe_volume.split_edges_n_times(2); + wireframe_surface.split_edges_n_times(2); + + (wireframe_volume, wireframe_surface) + }; + + let render_component = match_on_finite_element_model_3d!(&model, model => { + let accelerator = GeometryCollectionAccelerator::new(model); + PolyMesh3dComponent::new("render", render_surface_mesh) + .with_subfolder("render_meshes") + .with_interpolator(&accelerator)? + }); + + info!("Setting up model"); + let material = default_material(); + let bcs = match_on_finite_element_model_3d!(&model, model => { + set_up_boundary_conditions(model.vertices()) + }); + + // For now we're not interested in exporting the volume meshes as we want to save the space + let volume_poly_mesh = PolyMesh3d::from_poly_data(Vec::new(), NestedVec::new(), NestedVec::new()); + + let entity = BodyInitializer3d::initialize_in_state(&scene.initial_state) + .add_name(scene.name.clone()) + .add_finite_element_model(model, volume_poly_mesh)? + .set_material(material) + .add_boundary_conditions(bcs.initial_bc) + .entity(); + + scene + .simulation_systems + .add_system(Box::new(BoundaryConditionSwitchSystem { + time_for_switch: bcs.switch_time, + new_bcs: Some(bcs.final_bc), + entity, + })); + + scene + .initial_state + .insert_component(entity, PolyMesh3dCollection(vec![render_component])); + + { + // We don't want to export wireframes at every step, only at scene creation + let ply_dir = params.output_dir.join("ply"); + dump_polymesh_faces_ply(&wireframe_surface, &ply_dir, "wireframe_surface.ply")?; + dump_polymesh_faces_ply(&wireframe_volume, &ply_dir, "wireframe_volume.ply")?; + } + + Ok(entity) +} + +fn armadillo_slingshot_fem_tet4(params: &SceneParameters, name: &str, filename: &str) -> Result> { + let mut scene = initial_scene(name); + + let tet_mesh = load_mesh(params, filename)?; + let volume_poly_mesh = PolyMesh3d::from(&tet_mesh); + let tet_mesh = reorder_mesh_par(&tet_mesh).apply(&tet_mesh); + + let quadrature = tet_quadrature_strength_1(); + let fe_model = NodalModel::from_mesh_and_quadrature(tet_mesh.clone(), quadrature) + .with_mass_quadrature(tet_quadrature_strength_2()); + add_model(params, &mut scene, fe_model, &tet_mesh, volume_poly_mesh)?; + add_systems(&mut scene.simulation_systems); + Ok(scene) +} + +pub fn armadillo_slingshot_fem_tet4_500(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_fem_tet4_500"; + // TODO: This is currently just the cage + armadillo_slingshot_fem_tet4(params, &name, "cages/armadillo_slingshot_cage_500_fixed.msh") +} + +pub fn armadillo_slingshot_fem_tet4_1500(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_fem_tet4_1500"; + // TODO: This is currently just the cage + armadillo_slingshot_fem_tet4(params, &name, "cages/armadillo_slingshot_cage_1500_fixed.msh") +} + +pub fn armadillo_slingshot_fem_tet4_3000(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_fem_tet4_3000"; + // TODO: This is currently just the cage + armadillo_slingshot_fem_tet4(params, &name, "cages/armadillo_slingshot_cage_3000_fixed.msh") +} + +pub fn armadillo_slingshot_fem_tet4_5000(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_fem_tet4_5000"; + // TODO: This is currently just the cage + armadillo_slingshot_fem_tet4(params, &name, "cages/armadillo_slingshot_cage_5000_fixed.msh") +} + +pub fn armadillo_slingshot_fem_tet4_full(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_fem_tet4_full"; + armadillo_slingshot_fem_tet4(params, &name, "armadillo_slingshot.msh") +} + +fn armadillo_slingshot_fem_tet10( + params: &SceneParameters, + name: &str, + filename: &str, +) -> Result> { + let mut scene = initial_scene(name); + + let tet_mesh = load_mesh(params, filename)?; + let volume_poly_mesh = PolyMesh3d::from(&tet_mesh); + let tet_mesh = Tet10Mesh::from(&tet_mesh); + let tet_mesh = reorder_mesh_par(&tet_mesh).apply(&tet_mesh); + + // TODO: Use better + let quadrature = tet_quadrature_strength_5(); + let fe_model = NodalModel::from_mesh_and_quadrature(tet_mesh.clone(), quadrature) + .with_mass_quadrature(tet_quadrature_strength_5()); + add_model(params, &mut scene, fe_model, &tet_mesh, volume_poly_mesh)?; + add_systems(&mut scene.simulation_systems); + Ok(scene) +} + +pub fn armadillo_slingshot_fem_tet10_500(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_fem_tet10_500"; + armadillo_slingshot_fem_tet10(params, &name, "cages/armadillo_slingshot_cage_500_fixed.msh") +} + +pub fn armadillo_slingshot_fem_tet10_1000(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_fem_tet10_1000"; + armadillo_slingshot_fem_tet10(params, &name, "cages/armadillo_slingshot_cage_1000_fixed.msh") +} + +pub fn armadillo_slingshot_fem_tet10_3000(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_fem_tet10_3000"; + armadillo_slingshot_fem_tet10(params, &name, "cages/armadillo_slingshot_cage_3000_fixed.msh") +} + +pub fn armadillo_slingshot_fem_tet10_5000(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_fem_tet10_5000"; + armadillo_slingshot_fem_tet10(params, &name, "cages/armadillo_slingshot_cage_5000_fixed.msh") +} + +fn armadillo_slingshot_embedded_tet10( + params: &SceneParameters, + name: &str, + background_mesh_filename: &str, +) -> Result> { + let mut scene = initial_scene(name); + + let embedded_mesh = load_fine_embedded_mesh(params)?; + let volume_poly_mesh = PolyMesh3d::from(&embedded_mesh); + let background_mesh = load_mesh(params, background_mesh_filename)?; + let background_mesh = Tet10Mesh::from(&background_mesh); + let background_mesh = reorder_mesh_par(&background_mesh).apply(&background_mesh); + + // TODO: Use different stabilization options once we have different quadrature rules for + // mass/stiffness? + let embedding = embed_mesh_3d(&background_mesh, &volume_poly_mesh); + let mass_quadrature_opts = QuadratureOptions { + stabilization: Some(StabilizationOptions { + stabilization_factor: 1e-8, + stabilization_quadrature: tet_quadrature_strength_5(), + }), + }; + + let mass_quadrature = embed_quadrature_3d_with_opts( + &background_mesh, + &embedding, + tet_quadrature_strength_5(), + tet_quadrature_strength_5(), + &mass_quadrature_opts, + )?; + + let stiffness_quadrature_opts = QuadratureOptions { + stabilization: Some(StabilizationOptions { + stabilization_factor: 1e-8, + stabilization_quadrature: tet_quadrature_strength_2(), + }), + }; + + let stiffness_quadrature = embed_quadrature_3d_with_opts( + &background_mesh, + &embedding, + tet_quadrature_strength_3(), + tet_quadrature_strength_3(), + &stiffness_quadrature_opts, + )? + .simplified(3, &GlopSolver::new())?; + let elliptic_quadrature = stiffness_quadrature.clone(); + + let fe_model = EmbeddedModelBuilder::from_embedding(&background_mesh, embedding) + .mass_quadrature(mass_quadrature) + .stiffness_quadrature(stiffness_quadrature) + .elliptic_quadrature(elliptic_quadrature) + .build(); + + let schwarz = SchwarzPreconditionerComponent::from_embedded_model(&fe_model, 0.5); + let entity = add_model(params, &mut scene, fe_model, &background_mesh, volume_poly_mesh)?; + add_systems(&mut scene.simulation_systems); + scene.initial_state.insert_component(entity, schwarz); + + Ok(scene) +} + +pub fn armadillo_slingshot_embedded_tet10_500(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_embedded_tet10_500"; + armadillo_slingshot_embedded_tet10(params, &name, "cages/armadillo_slingshot_cage_500_fixed.msh") +} + +pub fn armadillo_slingshot_embedded_tet10_1000(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_embedded_tet10_1000"; + armadillo_slingshot_embedded_tet10(params, &name, "cages/armadillo_slingshot_cage_1000_fixed.msh") +} + +pub fn armadillo_slingshot_embedded_tet10_1500(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_embedded_tet10_1500"; + armadillo_slingshot_embedded_tet10(params, &name, "cages/armadillo_slingshot_cage_1500_fixed.msh") +} + +pub fn armadillo_slingshot_embedded_tet10_3000(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_embedded_tet10_3000"; + armadillo_slingshot_embedded_tet10(params, &name, "cages/armadillo_slingshot_cage_3000_fixed.msh") +} + +pub fn armadillo_slingshot_embedded_tet10_5000(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_embedded_tet10_5000"; + armadillo_slingshot_embedded_tet10(params, &name, "cages/armadillo_slingshot_cage_5000_fixed.msh") +} + +fn armadillo_slingshot_embedded_tet4( + params: &SceneParameters, + name: &str, + background_mesh_filename: &str, +) -> Result> { + let mut scene = initial_scene(name); + + let embedded_mesh = load_fine_embedded_mesh(params)?; + let volume_poly_mesh = PolyMesh3d::from(&embedded_mesh); + let background_mesh = load_mesh(params, background_mesh_filename)?; + let background_mesh = reorder_mesh_par(&background_mesh).apply(&background_mesh); + + let embedding = embed_mesh_3d(&background_mesh, &volume_poly_mesh); + + // TODO: We don't use any stabilization because that should not be necessary for linear + // tet elements. Is this correct? + + let mass_quadrature = embed_quadrature_3d( + &background_mesh, + &embedding, + tet_quadrature_strength_2(), + tet_quadrature_strength_2(), + )?; + + let stiffness_quadrature = embed_quadrature_3d( + &background_mesh, + &embedding, + tet_quadrature_strength_1(), + tet_quadrature_strength_1(), + )? + // We want to ensure that we obtain a single-point zeroth-order quadrature + .simplified(0, &GlopSolver::new())?; + let elliptic_quadrature = stiffness_quadrature.clone(); + + let fe_model = EmbeddedModelBuilder::from_embedding(&background_mesh, embedding) + .mass_quadrature(mass_quadrature) + .stiffness_quadrature(stiffness_quadrature) + .elliptic_quadrature(elliptic_quadrature) + .build(); + + add_model(params, &mut scene, fe_model, &background_mesh, volume_poly_mesh)?; + add_systems(&mut scene.simulation_systems); + Ok(scene) +} + +pub fn armadillo_slingshot_embedded_tet4_500(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_embedded_tet4_500"; + armadillo_slingshot_embedded_tet4(params, &name, "cages/armadillo_slingshot_cage_500_fixed.msh") +} + +pub fn armadillo_slingshot_embedded_tet4_1500(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_embedded_tet4_1500"; + armadillo_slingshot_embedded_tet4(params, &name, "cages/armadillo_slingshot_cage_1500_fixed.msh") +} + +pub fn armadillo_slingshot_embedded_tet4_3000(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_embedded_tet4_3000"; + armadillo_slingshot_embedded_tet4(params, &name, "cages/armadillo_slingshot_cage_3000_fixed.msh") +} + +pub fn armadillo_slingshot_embedded_tet4_5000(params: &SceneParameters) -> Result> { + let name = "armadillo_slingshot_embedded_tet4_5000"; + armadillo_slingshot_embedded_tet4(params, &name, "cages/armadillo_slingshot_cage_5000_fixed.msh") +} diff --git a/scene_runner/src/scenes/cantilever3d.rs b/scene_runner/src/scenes/cantilever3d.rs new file mode 100644 index 0000000..31a3d3e --- /dev/null +++ b/scene_runner/src/scenes/cantilever3d.rs @@ -0,0 +1,79 @@ +use std::convert::TryFrom; +use std::error::Error; + +use crate::scenes::{filtered_vertex_indices, Scene, SceneParameters}; + +use simulation_toolbox::components::Name; +use simulation_toolbox::fem::{FiniteElementIntegrator, FiniteElementMeshDeformer, Material}; + +use crate::scenes::helpers::BodyInitializer3d; +use fenris::geometry::polymesh::PolyMesh3d; +use fenris::geometry::procedural::create_rectangular_uniform_hex_mesh; +use fenris::mesh::Tet4Mesh; +use fenris::model::NodalModel; +use fenris::nalgebra::Vector3; +use fenris::quadrature::{hex_quadrature_strength_5, tet_quadrature_strength_5}; +use fenris::solid::materials::{StableNeoHookeanMaterial, YoungPoisson}; + +pub fn cantilever3d(_params: &SceneParameters) -> Result> { + let mut scene = Scene { + initial_state: Default::default(), + simulation_systems: Default::default(), + analysis_systems: Default::default(), + duration: 10.0, + name: String::from("cantilever3d"), + }; + + let volume_mesh = create_rectangular_uniform_hex_mesh(1.0, 4, 1, 1, 1); + let volume_poly_mesh = PolyMesh3d::from(&volume_mesh); + + let material = Material { + density: 1000.0, + mass_damping_coefficient: None, + stiffness_damping_coefficient: None, + elastic_model: StableNeoHookeanMaterial::from(YoungPoisson { + young: 3e6, + poisson: 0.4, + }) + .into(), + }; + + // Tet model + { + let tet_poly_mesh = volume_poly_mesh.triangulate()?; + let tet_mesh = Tet4Mesh::try_from(&tet_poly_mesh).unwrap(); + + let quadrature = tet_quadrature_strength_5(); + let fe_model = NodalModel::from_mesh_and_quadrature(tet_mesh.clone(), quadrature); + BodyInitializer3d::initialize_in_state(&scene.initial_state) + .add_name(Name("cantilever_tet4".to_string())) + .add_finite_element_model(fe_model, volume_poly_mesh.clone())? + .set_static_nodes(filtered_vertex_indices(tet_mesh.vertices(), |v| v.x < 1e-6)) + .set_material(material.clone()); + } + + // Hex model + { + let mut volume_mesh_hex = volume_mesh.clone(); + let static_nodes = filtered_vertex_indices(volume_mesh_hex.vertices(), |v| v.x < 1e-6); + volume_mesh_hex.translate(&Vector3::new(0.0, 2.0, 0.0)); + let quadrature = hex_quadrature_strength_5(); + + let fe_model = NodalModel::from_mesh_and_quadrature(volume_mesh_hex.clone(), quadrature); + + BodyInitializer3d::initialize_in_state(&scene.initial_state) + .add_name(Name("cantilever_hex8".to_string())) + .add_finite_element_model(fe_model, &volume_mesh_hex)? + .set_static_nodes(static_nodes) + .set_material(material.clone()); + } + + scene + .simulation_systems + .add_system(Box::new(FiniteElementIntegrator::default())); + scene + .simulation_systems + .add_system(Box::new(FiniteElementMeshDeformer)); + + Ok(scene) +} diff --git a/scene_runner/src/scenes/cylinder_shell.rs b/scene_runner/src/scenes/cylinder_shell.rs new file mode 100644 index 0000000..c931bee --- /dev/null +++ b/scene_runner/src/scenes/cylinder_shell.rs @@ -0,0 +1,1034 @@ +use std::error::Error; + +use crate::scenes::{filtered_vertex_indices, Scene, SceneConstructor, SceneParameters}; + +use simulation_toolbox::fem::{ + DirichletBoundaryConditionComponent, DirichletBoundaryConditions, FiniteElementIntegrator3d, + FiniteElementMeshDeformer, FiniteElementModel3d, IntegratorSettings, Material, +}; + +use crate::meshes::load_mesh_from_file; +use crate::scenes::helpers::BodyInitializer3d; +use core::fmt; +use fenris::connectivity::{Connectivity, ConnectivityMut}; +use fenris::embedding::{ + embed_mesh_3d, embed_quadrature_3d, embed_quadrature_3d_with_opts, EmbeddedModelBuilder, EmbeddedQuadrature, + QuadratureOptions, StabilizationOptions, +}; +use fenris::geometry::polymesh::PolyMesh3d; +use fenris::geometry::procedural::create_rectangular_uniform_hex_mesh; +use fenris::lp_solvers::GlopSolver; +use fenris::mesh::{Hex20Mesh, Hex27Mesh, HexMesh, Mesh3d, Tet10Mesh, Tet4Mesh}; +use fenris::model::NodalModel; +use fenris::nalgebra::{DVector, DVectorSliceMut, Point3, Vector3, U3}; +use fenris::nested_vec::NestedVec; +use fenris::quadrature::{ + hex_quadrature_strength_11, hex_quadrature_strength_3, hex_quadrature_strength_5, tet_quadrature_strength_1, + tet_quadrature_strength_10, tet_quadrature_strength_2, tet_quadrature_strength_3, tet_quadrature_strength_5, + Quadrature, +}; +use fenris::reorder::reorder_mesh_par; +use fenris::rtree::GeometryCollectionAccelerator; +use fenris::solid::materials::{StableNeoHookeanMaterial, YoungPoisson}; +use hamilton::{Entity, StorageContainer, System, Systems}; +use log::info; +use simulation_toolbox::components::{get_simulation_time, PolyMesh3dCollection, PolyMesh3dComponent}; +use simulation_toolbox::fem::bcs::{ + ConstantDisplacement, ConstantUniformAngularVelocity, ConstantUniformDisplacement, ConstantUniformVelocity, Union, +}; +use simulation_toolbox::io::ply::dump_polymesh_faces_ply; +use simulation_toolbox::match_on_finite_element_model_3d; + +pub fn scenes() -> Vec { + vec![ + SceneConstructor { + name: "cylinder_shell_fem_tet4_5k".to_string(), + constructor: cylinder_shell_fem_tet4_5k, + }, + SceneConstructor { + name: "cylinder_shell_fem_tet4_10k".to_string(), + constructor: cylinder_shell_fem_tet4_10k, + }, + SceneConstructor { + name: "cylinder_shell_fem_tet4_20k".to_string(), + constructor: cylinder_shell_fem_tet4_20k, + }, + SceneConstructor { + name: "cylinder_shell_fem_tet4_40k".to_string(), + constructor: cylinder_shell_fem_tet4_40k, + }, + SceneConstructor { + name: "cylinder_shell_fem_tet4_80k".to_string(), + constructor: cylinder_shell_fem_tet4_80k, + }, + SceneConstructor { + name: "cylinder_shell_fem_tet4_160k".to_string(), + constructor: cylinder_shell_fem_tet4_160k, + }, + SceneConstructor { + name: "cylinder_shell_fem_tet4_320k".to_string(), + constructor: cylinder_shell_fem_tet4_320k, + }, + SceneConstructor { + name: "cylinder_shell_fem_tet10_5k_strength2".to_string(), + constructor: cylinder_shell_fem_tet10_5k_strength2, + }, + SceneConstructor { + name: "cylinder_shell_fem_tet10_5k_strength3".to_string(), + constructor: cylinder_shell_fem_tet10_5k_strength3, + }, + SceneConstructor { + name: "cylinder_shell_fem_tet10_5k_strength5".to_string(), + constructor: cylinder_shell_fem_tet10_5k_strength5, + }, + SceneConstructor { + name: "cylinder_shell_fem_tet10_10k".to_string(), + constructor: cylinder_shell_fem_tet10_10k, + }, + SceneConstructor { + name: "cylinder_shell_fem_tet10_20k".to_string(), + constructor: cylinder_shell_fem_tet10_20k, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex8_res2".to_string(), + constructor: cylinder_shell_embedded_hex8_res2, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex8_res3".to_string(), + constructor: cylinder_shell_embedded_hex8_res3, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex8_res5".to_string(), + constructor: cylinder_shell_embedded_hex8_res5, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex8_res10".to_string(), + constructor: cylinder_shell_embedded_hex8_res10, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex8_res14".to_string(), + constructor: cylinder_shell_embedded_hex8_res14, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex8_res16".to_string(), + constructor: cylinder_shell_embedded_hex8_res16, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex8_res18".to_string(), + constructor: cylinder_shell_embedded_hex8_res18, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex8_res20".to_string(), + constructor: cylinder_shell_embedded_hex8_res20, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex8_res22".to_string(), + constructor: cylinder_shell_embedded_hex8_res22, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex8_res24".to_string(), + constructor: cylinder_shell_embedded_hex8_res24, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex8_res26".to_string(), + constructor: cylinder_shell_embedded_hex8_res26, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex8_res28".to_string(), + constructor: cylinder_shell_embedded_hex8_res28, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex8_res29".to_string(), + constructor: cylinder_shell_embedded_hex8_res29, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex8_res30".to_string(), + constructor: cylinder_shell_embedded_hex8_res30, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex8_res32".to_string(), + constructor: cylinder_shell_embedded_hex8_res32, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex20_res1".to_string(), + constructor: cylinder_shell_embedded_hex20_res1, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex20_res2".to_string(), + constructor: cylinder_shell_embedded_hex20_res2, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex20_res3".to_string(), + constructor: cylinder_shell_embedded_hex20_res3, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex20_res4".to_string(), + constructor: cylinder_shell_embedded_hex20_res4, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex20_res5".to_string(), + constructor: cylinder_shell_embedded_hex20_res5, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex20_res6_strength5".to_string(), + constructor: cylinder_shell_embedded_hex20_res6_strength5, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex20_res6_strength5_no_simp".to_string(), + constructor: cylinder_shell_embedded_hex20_res6_strength5_no_simp, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex20_res6_strength3".to_string(), + constructor: cylinder_shell_embedded_hex20_res6_strength3, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex20_res7_strength3".to_string(), + constructor: cylinder_shell_embedded_hex20_res7_strength3, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex20_res7_strength5".to_string(), + constructor: cylinder_shell_embedded_hex20_res7_strength5, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex20_res7_strength5_no_simp".to_string(), + constructor: cylinder_shell_embedded_hex20_res7_strength5_no_simp, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex20_res8".to_string(), + constructor: cylinder_shell_embedded_hex20_res8, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex20_res10".to_string(), + constructor: cylinder_shell_embedded_hex20_res10, + }, + SceneConstructor { + name: "cylinder_shell_embedded_hex27_res5".to_string(), + constructor: cylinder_shell_embedded_hex27_res5, + }, + ] +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug)] +enum QuadratureRule { + Tet1, + Tet2, + Tet3, + Tet5, + Tet10, + Hex3, + Hex5, + Hex11, +} + +impl QuadratureRule { + fn is_hex(&self) -> bool { + match self { + Self::Hex3 | Self::Hex5 | Self::Hex11 => true, + _ => false, + } + } + + fn is_tet(&self) -> bool { + !self.is_hex() + } + + fn construct_quadrature(&self) -> (Vec, Vec>) { + match self { + Self::Tet1 => tet_quadrature_strength_1(), + Self::Tet2 => tet_quadrature_strength_2(), + Self::Tet3 => tet_quadrature_strength_3(), + Self::Tet5 => tet_quadrature_strength_5(), + Self::Tet10 => tet_quadrature_strength_10(), + Self::Hex3 => hex_quadrature_strength_3(), + Self::Hex5 => hex_quadrature_strength_5(), + Self::Hex11 => hex_quadrature_strength_11(), + } + } +} + +#[derive(Clone, Copy, Debug)] +enum QuadratureStrength { + NonSimplified(QuadratureRule), + Simplified { source: QuadratureRule, simplified: u16 }, +} + +impl QuadratureStrength { + fn non_simplified(source: QuadratureRule) -> Self { + Self::NonSimplified(source) + } + + fn simplified(source: QuadratureRule, simplified: u16) -> Self { + Self::Simplified { source, simplified } + } + + fn hex5_simp4() -> Self { + Self::simplified(QuadratureRule::Hex5, 4) + } + + fn source_quadrature(&self) -> QuadratureRule { + match self { + Self::NonSimplified(q) => *q, + Self::Simplified { source, .. } => *source, + } + } + + fn is_simplified(&self) -> bool { + match self { + Self::NonSimplified(_) => false, + _ => true, + } + } +} + +fn initial_scene(name: &str) -> Scene { + Scene { + initial_state: Default::default(), + simulation_systems: Default::default(), + analysis_systems: Default::default(), + duration: 7.0, + name: String::from(name), + } +} + +fn default_material() -> Material { + Material { + density: 1000.0, + mass_damping_coefficient: None, + stiffness_damping_coefficient: Some(0.05), + elastic_model: StableNeoHookeanMaterial::from(YoungPoisson { + young: 5e6, + poisson: 0.48, + }) + .into(), + } +} + +fn add_systems(systems: &mut Systems) { + let settings = IntegratorSettings::default().set_project_stiffness(false); + systems.add_system(Box::new(FiniteElementIntegrator3d::with_settings(settings))); + systems.add_system(Box::new(FiniteElementMeshDeformer)); +} + +fn load_mesh(params: &SceneParameters, filename: &str) -> Result, Box> { + load_mesh_from_file(¶ms.asset_dir, &format!("meshes/cylinder_shell/{}", filename)) +} + +fn load_fine_embedded_mesh(params: &SceneParameters) -> Result, Box> { + load_mesh(params, "cylinder_shell_10k.msh") +} + +/// System that switches out BCs after a given simulation time +#[derive(Debug)] +pub struct BoundaryConditionSwitchSystem { + time_for_switch: f64, + new_bcs: Option>, + entity: Entity, +} + +impl fmt::Display for BoundaryConditionSwitchSystem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "BoundaryConditionSwitchSystem") + } +} + +impl System for BoundaryConditionSwitchSystem { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + let t = get_simulation_time(data); + let mut bcs = data + .get_component_storage::() + .borrow_mut(); + + if let Ok(t) = t { + if t >= self.time_for_switch { + if let Some(new_bcs) = self.new_bcs.take() { + let bc_component = bcs + .get_component_mut(self.entity) + .expect("Entity must be in simulation"); + bc_component.bc = new_bcs; + } + } + } + + Ok(()) + } +} + +struct BoundaryConditions { + // BCs at the beginning of the scene + initial_bc: Box, + // BCs after switch + final_bc: Box, + // Time when switch takes place + switch_time: f64, +} + +fn set_up_boundary_conditions(mesh_vertices: &[Point3]) -> BoundaryConditions { + let minus_nodes = filtered_vertex_indices(mesh_vertices, |v| v.y < -6.99); + let plus_nodes = filtered_vertex_indices(mesh_vertices, |v| v.y > 6.99); + + let minus_velocity = Vector3::new(0.0, 0.25, 0.0); + let minus_moving_bc = ConstantUniformVelocity::new(&minus_nodes, minus_velocity); + let plus_node_positions = plus_nodes + .iter() + .copied() + .map(|index| mesh_vertices[index]) + .collect(); + let plus_moving_bc = ConstantUniformAngularVelocity::new( + &plus_nodes, + Vector3::new(0.0, 0.8, 0.0), + Point3::new(0.0, 7.0, 0.0), + plus_node_positions, + ); + // Time when the cylinder stops moving/rotating + let t_final = 4.0; + let minus_displacement = t_final * minus_velocity; + let minus_static_bc = ConstantUniformDisplacement::new(&minus_nodes, minus_displacement); + + // We need to figure out the displacements of the rotating end + let mut plus_displacements = DVector::zeros(plus_moving_bc.nrows()); + plus_moving_bc.apply_displacement_bcs(DVectorSliceMut::from(&mut plus_displacements), t_final); + let plus_static_bc = ConstantDisplacement::new_3d(&plus_nodes, plus_displacements); + + let moving_bc = Union::try_new(vec![minus_moving_bc.into(), plus_moving_bc.into()]).unwrap(); + let static_bc = Union::try_new(vec![minus_static_bc.into(), plus_static_bc.into()]).unwrap(); + + BoundaryConditions { + initial_bc: Box::new(moving_bc), + final_bc: Box::new(static_bc), + switch_time: 4.0, + } +} + +fn add_model<'a, Model, C>( + params: &SceneParameters, + scene: &mut Scene, + model: Model, + mesh: &Mesh3d, + _volume_poly_mesh: PolyMesh3d, +) -> Result<(), Box> +where + Model: Into, + C: Connectivity, + C::FaceConnectivity: ConnectivityMut, +{ + let model = model.into(); + info!("Generating render mesh"); + let render_surface_mesh = PolyMesh3d::from_surface_mesh(&load_fine_embedded_mesh(¶ms)?.extract_surface_mesh()); + + info!("Generating wireframes"); + let (wireframe_volume, wireframe_surface) = { + let fe_mesh_volume = mesh.extract_face_soup(); + let fe_mesh_surface = mesh.extract_surface_mesh(); + let mut wireframe_volume = PolyMesh3d::from_surface_mesh(&fe_mesh_volume); + let mut wireframe_surface = PolyMesh3d::from_surface_mesh(&fe_mesh_surface); + wireframe_volume.split_edges_n_times(2); + wireframe_surface.split_edges_n_times(2); + + (wireframe_volume, wireframe_surface) + }; + + let render_component = match_on_finite_element_model_3d!(&model, model => { + let accelerator = GeometryCollectionAccelerator::new(model); + PolyMesh3dComponent::new("render", render_surface_mesh) + .with_subfolder("render_meshes") + .with_interpolator(&accelerator)? + }); + + info!("Setting up model"); + let material = default_material(); + let bcs = match_on_finite_element_model_3d!(&model, model => { + set_up_boundary_conditions(model.vertices()) + }); + + // We don't need the poly mesh, so let's not clutter up the output storage + let volume_poly_mesh = PolyMesh3d::from_poly_data(Vec::new(), NestedVec::new(), NestedVec::new()); + let entity = BodyInitializer3d::initialize_in_state(&scene.initial_state) + .add_name(scene.name.clone()) + .add_finite_element_model(model, volume_poly_mesh)? + .set_material(material) + .add_boundary_conditions(bcs.initial_bc) + .entity(); + + scene + .simulation_systems + .add_system(Box::new(BoundaryConditionSwitchSystem { + time_for_switch: bcs.switch_time, + new_bcs: Some(bcs.final_bc), + entity, + })); + + scene + .initial_state + .insert_component(entity, PolyMesh3dCollection(vec![render_component])); + + { + // We don't want to export wireframes at every step, only at scene creation + let ply_dir = params.output_dir.join("ply"); + dump_polymesh_faces_ply(&wireframe_surface, &ply_dir, "wireframe_surface.ply")?; + dump_polymesh_faces_ply(&wireframe_volume, &ply_dir, "wireframe_volume.ply")?; + } + + Ok(()) +} + +fn create_background_mesh(resolution: usize) -> HexMesh { + let thickness = 2.0; + let mut mesh = create_rectangular_uniform_hex_mesh(thickness, 1, 8, 1, resolution); + mesh.translate(&Vector3::new(-thickness / 2.0, -8.0, -thickness / 2.0)); + mesh +} + +fn count_interface_points(quadrature: &EmbeddedQuadrature) -> usize { + quadrature + .interface_quadratures() + .iter() + .map(|quadrature| quadrature.points().len()) + .sum::() +} + +fn cylinder_shell_fem_tet4(params: &SceneParameters, name: &str, filename: &str) -> Result> { + let mut scene = initial_scene(name); + + let tet_mesh = load_mesh(params, filename)?; + let volume_poly_mesh = PolyMesh3d::from(&tet_mesh); + let tet_mesh = reorder_mesh_par(&tet_mesh).apply(&tet_mesh); + + let quadrature = tet_quadrature_strength_1(); + let fe_model = NodalModel::from_mesh_and_quadrature(tet_mesh.clone(), quadrature) + .with_mass_quadrature(tet_quadrature_strength_2()); + add_model(params, &mut scene, fe_model, &tet_mesh, volume_poly_mesh)?; + add_systems(&mut scene.simulation_systems); + Ok(scene) +} + +pub fn cylinder_shell_fem_tet4_5k(params: &SceneParameters) -> Result> { + let name = "cylinder_shell_fem_tet4_5k"; + cylinder_shell_fem_tet4(params, &name, "cylinder_shell_5k.msh") +} + +pub fn cylinder_shell_fem_tet4_10k(params: &SceneParameters) -> Result> { + let name = "cylinder_shell_fem_tet4_10k"; + cylinder_shell_fem_tet4(params, &name, "cylinder_shell_10k.msh") +} + +pub fn cylinder_shell_fem_tet4_20k(params: &SceneParameters) -> Result> { + let name = "cylinder_shell_fem_tet4_20k"; + cylinder_shell_fem_tet4(params, &name, "cylinder_shell_20k.msh") +} + +pub fn cylinder_shell_fem_tet4_40k(params: &SceneParameters) -> Result> { + let name = "cylinder_shell_fem_tet4_40k"; + cylinder_shell_fem_tet4(params, &name, "cylinder_shell_40k.msh") +} + +pub fn cylinder_shell_fem_tet4_80k(params: &SceneParameters) -> Result> { + let name = "cylinder_shell_fem_tet4_80k"; + cylinder_shell_fem_tet4(params, &name, "cylinder_shell_80k.msh") +} + +pub fn cylinder_shell_fem_tet4_160k(params: &SceneParameters) -> Result> { + let name = "cylinder_shell_fem_tet4_160k"; + cylinder_shell_fem_tet4(params, &name, "cylinder_shell_160k.msh") +} + +pub fn cylinder_shell_fem_tet4_320k(params: &SceneParameters) -> Result> { + let name = "cylinder_shell_fem_tet4_320k"; + cylinder_shell_fem_tet4(params, &name, "cylinder_shell_320k.msh") +} + +fn cylinder_shell_embedded_hex8( + params: &SceneParameters, + name: &str, + resolution: usize, +) -> Result> { + let mut scene = initial_scene(name); + + let tet_mesh = load_fine_embedded_mesh(params)?; + let volume_poly_mesh = PolyMesh3d::from(&tet_mesh); + + let background_mesh = create_background_mesh(resolution); + + // Note: For now we only stabilize the mass matrix, which seems to be sufficient when + // using a direct solver + let mass_quadrature_opts = QuadratureOptions { + stabilization: Some(StabilizationOptions { + stabilization_factor: 1e-8, + stabilization_quadrature: hex_quadrature_strength_5(), + }), + }; + + info!("Embedding mesh..."); + let embedding = embed_mesh_3d(&background_mesh, &volume_poly_mesh); + info!("Constructing mass quadrature..."); + let mass_quadrature = embed_quadrature_3d_with_opts( + &background_mesh, + &embedding, + hex_quadrature_strength_5(), + tet_quadrature_strength_5(), + &mass_quadrature_opts, + )?; + info!( + "Constructed mass quadrature: {} points in interface elements.", + count_interface_points(&mass_quadrature) + ); + + info!("Constructing stiffness quadrature..."); + let stiffness_quadrature = embed_quadrature_3d( + &background_mesh, + &embedding, + hex_quadrature_strength_3(), + tet_quadrature_strength_2(), + )?; + info!( + "Constructed stiffness quadrature: {} points in interface elements.", + count_interface_points(&stiffness_quadrature) + ); + info!("Simplifying stiffness quadrature..."); + let stiffness_quadrature = stiffness_quadrature.simplified(2, &GlopSolver::new())?; + info!( + "Simplified stiffness quadrature: {} points in interface elements", + count_interface_points(&stiffness_quadrature) + ); + let elliptic_quadrature = stiffness_quadrature.clone(); + + let fe_model = EmbeddedModelBuilder::from_embedding(&background_mesh, embedding) + .mass_quadrature(mass_quadrature) + .stiffness_quadrature(stiffness_quadrature) + .elliptic_quadrature(elliptic_quadrature) + .build(); + + let mesh = fe_model.background_mesh().clone(); + add_model(params, &mut scene, fe_model, &mesh, volume_poly_mesh)?; + add_systems(&mut scene.simulation_systems); + Ok(scene) +} + +pub fn cylinder_shell_embedded_hex8_res2(params: &SceneParameters) -> Result> { + let resolution = 2; + cylinder_shell_embedded_hex8(params, "cylinder_shell_embedded_hex8_res2", resolution) +} + +pub fn cylinder_shell_embedded_hex8_res3(params: &SceneParameters) -> Result> { + let resolution = 3; + cylinder_shell_embedded_hex8(params, "cylinder_shell_embedded_hex8_res3", resolution) +} + +pub fn cylinder_shell_embedded_hex8_res5(params: &SceneParameters) -> Result> { + let resolution = 5; + cylinder_shell_embedded_hex8(params, "cylinder_shell_embedded_hex8_res5", resolution) +} + +pub fn cylinder_shell_embedded_hex8_res10(params: &SceneParameters) -> Result> { + let resolution = 10; + cylinder_shell_embedded_hex8(params, "cylinder_shell_embedded_hex8_res10", resolution) +} + +pub fn cylinder_shell_embedded_hex8_res14(params: &SceneParameters) -> Result> { + let resolution = 14; + cylinder_shell_embedded_hex8(params, "cylinder_shell_embedded_hex8_res14", resolution) +} + +pub fn cylinder_shell_embedded_hex8_res16(params: &SceneParameters) -> Result> { + let resolution = 16; + cylinder_shell_embedded_hex8(params, "cylinder_shell_embedded_hex8_res16", resolution) +} + +pub fn cylinder_shell_embedded_hex8_res18(params: &SceneParameters) -> Result> { + let resolution = 18; + cylinder_shell_embedded_hex8(params, "cylinder_shell_embedded_hex8_res18", resolution) +} + +pub fn cylinder_shell_embedded_hex8_res20(params: &SceneParameters) -> Result> { + let resolution = 20; + cylinder_shell_embedded_hex8(params, "cylinder_shell_embedded_hex8_res20", resolution) +} + +pub fn cylinder_shell_embedded_hex8_res22(params: &SceneParameters) -> Result> { + let resolution = 22; + cylinder_shell_embedded_hex8(params, "cylinder_shell_embedded_hex8_res22", resolution) +} + +pub fn cylinder_shell_embedded_hex8_res24(params: &SceneParameters) -> Result> { + let resolution = 24; + cylinder_shell_embedded_hex8(params, "cylinder_shell_embedded_hex8_res24", resolution) +} + +pub fn cylinder_shell_embedded_hex8_res26(params: &SceneParameters) -> Result> { + let resolution = 26; + cylinder_shell_embedded_hex8(params, "cylinder_shell_embedded_hex8_res26", resolution) +} + +pub fn cylinder_shell_embedded_hex8_res28(params: &SceneParameters) -> Result> { + let resolution = 28; + cylinder_shell_embedded_hex8(params, "cylinder_shell_embedded_hex8_res28", resolution) +} + +pub fn cylinder_shell_embedded_hex8_res29(params: &SceneParameters) -> Result> { + let resolution = 29; + cylinder_shell_embedded_hex8(params, "cylinder_shell_embedded_hex8_res29", resolution) +} + +pub fn cylinder_shell_embedded_hex8_res30(params: &SceneParameters) -> Result> { + let resolution = 30; + cylinder_shell_embedded_hex8(params, "cylinder_shell_embedded_hex8_res30", resolution) +} + +pub fn cylinder_shell_embedded_hex8_res32(params: &SceneParameters) -> Result> { + let resolution = 32; + cylinder_shell_embedded_hex8(params, "cylinder_shell_embedded_hex8_res32", resolution) +} + +fn cylinder_shell_embedded_hex27( + params: &SceneParameters, + name: &str, + resolution: usize, +) -> Result> { + let mut scene = initial_scene(name); + + let tet_mesh = load_fine_embedded_mesh(params)?; + let volume_poly_mesh = PolyMesh3d::from(&tet_mesh); + + let background_mesh = create_background_mesh(resolution); + let background_mesh = Hex27Mesh::from(&background_mesh); + + let embedding = embed_mesh_3d(&background_mesh, &volume_poly_mesh); + + let mass_quadrature = embed_quadrature_3d( + &background_mesh, + &embedding, + hex_quadrature_strength_11(), + tet_quadrature_strength_10(), + )?; + // .simplified(10, &GlopSolver::new())?; + let stiffness_quadrature = embed_quadrature_3d( + &background_mesh, + &embedding, + hex_quadrature_strength_5(), + tet_quadrature_strength_5(), + )? + .simplified(5, &GlopSolver::new())?; + let elliptic_quadrature = stiffness_quadrature.clone(); + + let mut fe_model = EmbeddedModelBuilder::from_embedding(&background_mesh, embedding) + .mass_quadrature(mass_quadrature) + .stiffness_quadrature(stiffness_quadrature) + .elliptic_quadrature(elliptic_quadrature) + .build(); + + fe_model.set_mass_regularization_factor(1e-6); + + let mesh = fe_model.background_mesh().clone(); + add_model(params, &mut scene, fe_model, &mesh, volume_poly_mesh)?; + add_systems(&mut scene.simulation_systems); + Ok(scene) +} + +pub fn cylinder_shell_embedded_hex27_res5(params: &SceneParameters) -> Result> { + let resolution = 5; + cylinder_shell_embedded_hex27(params, "cylinder_shell_embedded_hex27_res5", resolution) +} + +fn cylinder_shell_embedded_hex20( + params: &SceneParameters, + name: &str, + resolution: usize, + quadrature_strength: QuadratureStrength, +) -> Result> { + let mut scene = initial_scene(name); + + let tet_mesh = load_fine_embedded_mesh(params)?; + let volume_poly_mesh = PolyMesh3d::from(&tet_mesh); + + let background_mesh = create_background_mesh(resolution); + let background_mesh = Hex20Mesh::from(&background_mesh); + + info!("Embedding mesh..."); + let embedding = embed_mesh_3d(&background_mesh, &volume_poly_mesh); + + let mass_quadrature_opts = QuadratureOptions { + stabilization: Some(StabilizationOptions { + stabilization_factor: 1e-8, + stabilization_quadrature: hex_quadrature_strength_11(), + }), + }; + info!("Constructing mass quadrature..."); + let mass_quadrature = embed_quadrature_3d_with_opts( + &background_mesh, + &embedding, + hex_quadrature_strength_11(), + tet_quadrature_strength_10(), + &mass_quadrature_opts, + )?; + info!( + "Constructed mass quadrature: {} points in interface elements.", + count_interface_points(&mass_quadrature) + ); + // .simplified(10, &GlopSolver::new())?; + + info!("Constructing stiffness quadrature {:?}...", quadrature_strength); + let stiffness_quadrature = match quadrature_strength.source_quadrature() { + QuadratureRule::Hex3 => embed_quadrature_3d( + &background_mesh, + &embedding, + hex_quadrature_strength_3(), + tet_quadrature_strength_3(), + )?, + QuadratureRule::Hex5 => embed_quadrature_3d( + &background_mesh, + &embedding, + hex_quadrature_strength_5(), + tet_quadrature_strength_5(), + )?, + QuadratureRule::Hex11 => embed_quadrature_3d( + &background_mesh, + &embedding, + hex_quadrature_strength_11(), + tet_quadrature_strength_10(), + )?, + _ => { + return Err(Box::from(format!( + "Unsupported quadrature for cylinder_shell_embedded_hex20: {:?}", + quadrature_strength + ))) + } + }; + info!( + "Constructed stiffness quadrature: {} points in interface elements.", + count_interface_points(&stiffness_quadrature) + ); + + let stiffness_quadrature = if let QuadratureStrength::Simplified { source: _, simplified } = quadrature_strength { + info!("Simplifying stiffness quadrature to strength {}...", simplified); + let simplified = stiffness_quadrature.simplified(simplified as usize, &GlopSolver::new())?; + info!( + "Simplified stiffness quadrature: {} points in interface elements", + count_interface_points(&simplified) + ); + simplified + } else { + info!("Skipping stiffness quadrature simplification!"); + stiffness_quadrature + }; + + let elliptic_quadrature = stiffness_quadrature.clone(); + + let fe_model = EmbeddedModelBuilder::from_embedding(&background_mesh, embedding) + .mass_quadrature(mass_quadrature) + .stiffness_quadrature(stiffness_quadrature) + .elliptic_quadrature(elliptic_quadrature) + .build(); + + let mesh = fe_model.background_mesh().clone(); + add_model(params, &mut scene, fe_model, &mesh, volume_poly_mesh)?; + add_systems(&mut scene.simulation_systems); + Ok(scene) +} + +pub fn cylinder_shell_embedded_hex20_res1(params: &SceneParameters) -> Result> { + let resolution = 1; + cylinder_shell_embedded_hex20( + params, + "cylinder_shell_embedded_hex20_res1", + resolution, + QuadratureStrength::hex5_simp4(), + ) +} + +pub fn cylinder_shell_embedded_hex20_res2(params: &SceneParameters) -> Result> { + let resolution = 2; + cylinder_shell_embedded_hex20( + params, + "cylinder_shell_embedded_hex20_res2", + resolution, + QuadratureStrength::hex5_simp4(), + ) +} + +pub fn cylinder_shell_embedded_hex20_res3(params: &SceneParameters) -> Result> { + let resolution = 3; + cylinder_shell_embedded_hex20( + params, + "cylinder_shell_embedded_hex20_res3", + resolution, + QuadratureStrength::hex5_simp4(), + ) +} + +pub fn cylinder_shell_embedded_hex20_res4(params: &SceneParameters) -> Result> { + let resolution = 4; + cylinder_shell_embedded_hex20( + params, + "cylinder_shell_embedded_hex20_res4", + resolution, + QuadratureStrength::hex5_simp4(), + ) +} + +pub fn cylinder_shell_embedded_hex20_res5(params: &SceneParameters) -> Result> { + let resolution = 5; + cylinder_shell_embedded_hex20( + params, + "cylinder_shell_embedded_hex20_res5", + resolution, + QuadratureStrength::hex5_simp4(), + ) +} + +pub fn cylinder_shell_embedded_hex20_res6_strength3(params: &SceneParameters) -> Result> { + let resolution = 6; + cylinder_shell_embedded_hex20( + params, + "cylinder_shell_embedded_hex20_res6_strength3", + resolution, + QuadratureStrength::simplified(QuadratureRule::Hex3, 3), + ) +} + +pub fn cylinder_shell_embedded_hex20_res6_strength5(params: &SceneParameters) -> Result> { + let resolution = 6; + cylinder_shell_embedded_hex20( + params, + "cylinder_shell_embedded_hex20_res6_strength5", + resolution, + QuadratureStrength::hex5_simp4(), + ) +} + +pub fn cylinder_shell_embedded_hex20_res6_strength5_no_simp(params: &SceneParameters) -> Result> { + let resolution = 6; + cylinder_shell_embedded_hex20( + params, + "cylinder_shell_embedded_hex20_res6_strength5_no_simp", + resolution, + QuadratureStrength::non_simplified(QuadratureRule::Hex5), + ) +} + +pub fn cylinder_shell_embedded_hex20_res7_strength3(params: &SceneParameters) -> Result> { + let resolution = 7; + cylinder_shell_embedded_hex20( + params, + "cylinder_shell_embedded_hex20_res7_strength3", + resolution, + QuadratureStrength::simplified(QuadratureRule::Hex3, 3), + ) +} + +pub fn cylinder_shell_embedded_hex20_res7_strength5(params: &SceneParameters) -> Result> { + let resolution = 7; + cylinder_shell_embedded_hex20( + params, + "cylinder_shell_embedded_hex20_res7_strength5", + resolution, + QuadratureStrength::hex5_simp4(), + ) +} + +pub fn cylinder_shell_embedded_hex20_res7_strength5_no_simp(params: &SceneParameters) -> Result> { + let resolution = 7; + cylinder_shell_embedded_hex20( + params, + "cylinder_shell_embedded_hex20_res7_strength5_no_simp", + resolution, + QuadratureStrength::non_simplified(QuadratureRule::Hex5), + ) +} + +pub fn cylinder_shell_embedded_hex20_res8(params: &SceneParameters) -> Result> { + let resolution = 8; + cylinder_shell_embedded_hex20( + params, + "cylinder_shell_embedded_hex20_res8", + resolution, + QuadratureStrength::hex5_simp4(), + ) +} + +pub fn cylinder_shell_embedded_hex20_res10(params: &SceneParameters) -> Result> { + let resolution = 10; + cylinder_shell_embedded_hex20( + params, + "cylinder_shell_embedded_hex20_res10", + resolution, + QuadratureStrength::hex5_simp4(), + ) +} + +fn cylinder_shell_fem_tet10( + params: &SceneParameters, + name: &str, + filename: &str, + quadrature_strength: QuadratureStrength, +) -> Result> { + let mut scene = initial_scene(name); + + let tet_mesh = load_mesh(params, filename)?; + let volume_poly_mesh = PolyMesh3d::from(&tet_mesh); + let tet_mesh = Tet10Mesh::from(&tet_mesh); + + let tet_mesh = reorder_mesh_par(&tet_mesh).apply(&tet_mesh); + + if !quadrature_strength.source_quadrature().is_tet() || quadrature_strength.is_simplified() { + return Err(Box::from(format!( + "Unsupported quadrature for cylinder_shell_fem_tet10: {:?}", + quadrature_strength + ))); + } + + info!("Constructing stiffness quadrature {:?}...", quadrature_strength); + let quadrature = quadrature_strength + .source_quadrature() + .construct_quadrature(); + + let fe_model = NodalModel::from_mesh_and_quadrature(tet_mesh.clone(), quadrature); + // TODO: Maybe use order 4 quadrature? (Or even 3?) + add_model(params, &mut scene, fe_model, &tet_mesh, volume_poly_mesh)?; + add_systems(&mut scene.simulation_systems); + Ok(scene) +} + +pub fn cylinder_shell_fem_tet10_5k_strength2(params: &SceneParameters) -> Result> { + cylinder_shell_fem_tet10( + params, + "cylinder_shell_fem_tet10_5k_strength2", + "cylinder_shell_5k.msh", + QuadratureStrength::non_simplified(QuadratureRule::Tet2), + ) +} + +pub fn cylinder_shell_fem_tet10_5k_strength3(params: &SceneParameters) -> Result> { + cylinder_shell_fem_tet10( + params, + "cylinder_shell_fem_tet10_5k_strength3", + "cylinder_shell_5k.msh", + QuadratureStrength::non_simplified(QuadratureRule::Tet3), + ) +} + +pub fn cylinder_shell_fem_tet10_5k_strength5(params: &SceneParameters) -> Result> { + cylinder_shell_fem_tet10( + params, + "cylinder_shell_fem_tet10_5k_strength5", + "cylinder_shell_5k.msh", + QuadratureStrength::non_simplified(QuadratureRule::Tet5), + ) +} + +pub fn cylinder_shell_fem_tet10_10k(params: &SceneParameters) -> Result> { + cylinder_shell_fem_tet10( + params, + "cylinder_shell_fem_tet10_10k", + "cylinder_shell_10k.msh", + QuadratureStrength::non_simplified(QuadratureRule::Tet5), + ) +} + +pub fn cylinder_shell_fem_tet10_20k(params: &SceneParameters) -> Result> { + cylinder_shell_fem_tet10( + params, + "cylinder_shell_fem_tet10_20k", + "cylinder_shell_20k.msh", + QuadratureStrength::non_simplified(QuadratureRule::Tet5), + ) +} diff --git a/scene_runner/src/scenes/helpers.rs b/scene_runner/src/scenes/helpers.rs new file mode 100644 index 0000000..7b93148 --- /dev/null +++ b/scene_runner/src/scenes/helpers.rs @@ -0,0 +1,691 @@ +use std::error::Error; +use std::mem; +use std::path::{Path, PathBuf}; + +use fenris::allocators::ElementConnectivityAllocator; +use fenris::element::ElementConnectivity; +use fenris::embedding::{find_background_cell_indices_2d, EmbeddedModel}; +use fenris::geometry::procedural::{create_rectangular_uniform_quad_mesh_2d, voxelize_bounding_box_2d}; +use fenris::geometry::vtk::{create_vtk_data_set_from_quadratures, write_vtk}; +use fenris::geometry::{AxisAlignedBoundingBox, AxisAlignedBoundingBox2d, BoundedGeometry}; +use fenris::mesh::{ClosedSurfaceMesh2d, QuadMesh2d, TriangleMesh2d}; +use fenris::model::FiniteElementInterpolator; +use fenris::nalgebra::allocator::Allocator; +use fenris::nalgebra::{ + convert, try_convert, DVector, DefaultAllocator, DimMin, DimName, Point, RealField, Vector2, Vector3, VectorN, +}; +use fenris::rtree::GeometryCollectionAccelerator; +use fenris::solid::ElasticityModel; +use fenris::util::flatten_vertically; +use hamilton::{register_component, BijectiveStorageMut, Entity, StorageContainer}; +use numeric_literals::replace_float_literals; +use simulation_toolbox::components::{ + Name, PointInterpolator, PolyMesh3dCollection, SimulationTime, StepIndex, SurfaceMesh2d, TimeStep, VolumeMesh2d, + VolumeMesh3d, +}; +use simulation_toolbox::fem::bcs::{Empty, Homogeneous, Union}; +use simulation_toolbox::fem::{ + DirichletBoundaryConditionComponent, DirichletBoundaryConditions, FiniteElementElasticModel2d, + FiniteElementElasticModel3d, FiniteElementModel2d, FiniteElementModel3d, IntegrationMethod, Material, +}; +use simulation_toolbox::{match_on_finite_element_model_2d, match_on_finite_element_model_3d}; + +pub fn register_known_components() -> Result<(), Box> { + register_component::()?; + register_component::()?; + register_component::()?; + register_component::()?; + register_component::()?; + register_component::()?; + register_component::()?; + register_component::()?; + register_component::()?; + register_component::()?; + register_component::()?; + register_component::()?; + + Ok(()) +} + +/// Provides utility functions for point clouds +pub struct PointHelper +where + D: DimName, + DefaultAllocator: Allocator, +{ + phantom: std::marker::PhantomData, +} + +impl PointHelper +where + D: DimName, + DefaultAllocator: Allocator, +{ + pub fn bb(points: &[Point]) -> Option> { + AxisAlignedBoundingBox::from_points(points) + } + + /// Uniformly scales the point cloud with the given scaling factor + pub fn scale(points: &mut [Point], scaling: f64) { + for p in points.iter_mut() { + p.coords *= scaling; + } + } + + /// Uniformly scales the point cloud such that the max extent of its bounding box corresponds to the specified value, returns scaling factor + pub fn scale_max_extent_to(points: &mut [Point], target_extent: f64) -> Option { + Self::bb(points).map(|bb| { + let max_extent = bb.max_extent(); + let scaling = target_extent / max_extent; + Self::scale(points, scaling); + scaling + }) + } + + /// Translates the point cloud by the vector + pub fn translate(points: &mut [Point], translation: &VectorN) { + for p in points.iter_mut() { + p.coords += translation; + } + } + + /// Translates the point cloud's bounding box center to the origin, returns the translation + pub fn center_to_origin(points: &mut [Point]) -> Option> { + Self::bb(points).map(|bb| { + let c = bb.center(); + let dx = -c.coords; + Self::translate(points, &dx); + dx + }) + } +} + +/// Generates a 2d quad background mesh to embed a 2d triangle mesh +pub fn generate_background_mesh_for_tri2d( + triangle_mesh: &TriangleMesh2d, + background_resolution: T, +) -> Result, Box> +where + T: RealField, +{ + let voxel_mesh = voxelize_bounding_box_2d(&triangle_mesh.bounding_box(), background_resolution); + + // Make sure to remove background cells that don't intersect the embedded mesh at all + let indices = find_background_cell_indices_2d(&voxel_mesh, &triangle_mesh)?; + Ok(voxel_mesh.keep_cells(&indices)) +} + +/// Generates a 2d quad background mesh to embed a 2d triangle mesh with just one cell +#[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] +pub fn generate_single_cell_background_mesh_for_tri2d( + triangle_mesh: &TriangleMesh2d, +) -> Result, Box> +where + T: RealField, +{ + let bounds = triangle_mesh.bounding_box(); + let extents = bounds.extents(); + let enlarged_bounds = AxisAlignedBoundingBox2d::new(bounds.min() - extents * 0.01, bounds.max() + extents * 0.01); + let enlarged_extents = enlarged_bounds.extents(); + + let enlarged_extents_f64: Vector2 = try_convert(enlarged_extents).expect("Must be able to fit extents in f64"); + + let cell_size_f64 = enlarged_extents_f64.x.max(enlarged_extents_f64.y); + let cell_size: T = convert(cell_size_f64); + + let center = bounds.center(); + let top_left = Vector2::new(center.x - cell_size / 2.0, center.y + cell_size / 2.0); + + Ok(create_rectangular_uniform_quad_mesh_2d( + convert(cell_size), + 1, + 1, + 1, + &top_left, + )) +} + +#[allow(dead_code)] +pub fn dump_embedded_interface_quadrature( + model: &EmbeddedModel, + path: impl AsRef, + title: impl AsRef, +) -> Result<(), Box> +where + T: RealField, + D: DimName + DimMin, + C: ElementConnectivity, + DefaultAllocator: Allocator + ElementConnectivityAllocator, +{ + let path: &Path = path.as_ref(); + let stem = path + .file_stem() + .ok_or_else(|| Box::::from("Path must be a file path"))?; + + let dump_rules = |name, quadrature| -> Result<(), Box> { + let dataset = + create_vtk_data_set_from_quadratures(model.vertices(), model.interface_connectivity(), quadrature); + let mut out_file_name = stem.to_os_string(); + out_file_name.push("_"); + out_file_name.push(name); + if let Some(extension) = path.extension() { + out_file_name.push("."); + out_file_name.push(extension); + } + let out_path = if let Some(parent) = path.parent() { + parent.join(&out_file_name) + } else { + PathBuf::from(out_file_name) + }; + write_vtk(dataset, &out_path, title.as_ref())?; + Ok(()) + }; + + if let Some(mass_quadrature) = model.mass_quadrature() { + dump_rules("mass", mass_quadrature.interface_quadratures())?; + } + + if let Some(stiffness_quadrature) = model.stiffness_quadrature() { + dump_rules("stiffness", stiffness_quadrature.interface_quadratures())?; + } + + if let Some(elliptic_quadrature) = model.elliptic_quadrature() { + dump_rules("elliptic", elliptic_quadrature.interface_quadratures())?; + } + + Ok(()) +} + +/// Helper struct to reduce boilerplate when initializing bodies. +pub struct BodyInitializer2d<'a> { + state: &'a StorageContainer, + entity: Entity, +} + +#[allow(dead_code)] +impl<'a> BodyInitializer2d<'a> { + pub fn initialize_in_state(state: &'a StorageContainer) -> Self { + // Register components as a convenience. Double-registering is unproblematic. + register_known_components().expect("Failed to register components. Should not happen"); + Self { + state, + entity: Entity::new(), + } + } + + pub fn entity(&self) -> Entity { + self.entity + } + + /// Sets the displacement components for all dofs of the attached finite element model + pub fn set_displacement(&self, displacement: &DVector) -> Result<&Self, Box> { + let mut fe_models = self + .state + .get_component_storage::() + .borrow_mut(); + + if let Some(model) = fe_models.get_component_mut(self.entity) { + if displacement.len() == model.u.len() { + model.u = displacement.clone(); + } else { + return Err(Box::from( + "Cannot set displacement: Supplied displacement vector does not have the right dimensions.", + )); + } + + Ok(self) + } else { + Err(Box::from( + "Cannot set displacement: Entity does not have a finite element model yet.", + )) + } + } + + /// Sets the velocity components for all dofs of the attached finite element model + pub fn set_velocity(&self, velocity: &DVector) -> Result<&Self, Box> { + let mut fe_models = self + .state + .get_component_storage::() + .borrow_mut(); + + if let Some(model) = fe_models.get_component_mut(self.entity) { + if velocity.len() == model.v.len() { + model.v = velocity.clone(); + } else { + return Err(Box::from( + "Cannot set velocity: Supplied velocity vector does not have the right dimensions.", + )); + } + + Ok(self) + } else { + Err(Box::from( + "Cannot set velocity: Entity does not have a finite element model yet.", + )) + } + } + + /// Adds an elastic finite element model and a volume mesh component to the entity. + pub fn add_finite_element_model( + &self, + fe_model: impl Into, + material_volume_mesh: impl Into, + ) -> Result<&Self, Box> { + let mut fe_models = self + .state + .get_component_storage::() + .borrow_mut(); + let mut volume_meshes = self + .state + .get_component_storage::() + .borrow_mut(); + + if fe_models.get_component(self.entity).is_some() { + panic!("Cannot add finite elment model: Entity already has finite element model."); + } else { + let fe_model = fe_model.into(); + let material_volume_mesh = material_volume_mesh.into(); + let ndof = fe_model.ndof(); + + let mesh_interpolator = match_on_finite_element_model_2d!(fe_model, fe_model => { + fe_model.make_interpolator(material_volume_mesh.vertices())? + }); + + let model = FiniteElementElasticModel2d { + model: fe_model, + u: DVector::zeros(ndof), + v: DVector::zeros(ndof), + factorization: None, + material_volume_mesh: material_volume_mesh.clone(), + material_volume_interpolator: mesh_interpolator, + material_surface: None, + material_surface_interpolator: None, + material: Material::default(), + integrator: Default::default(), + gravity_enabled: true, + model_matrix_storage: None, + }; + + fe_models.insert(self.entity, model); + volume_meshes.insert(self.entity, material_volume_mesh); + + Ok(self) + } + } + + pub fn add_boundary_conditions(&self, new_bc: Box) -> &Self { + let mut storage = self + .state + .get_component_storage::() + .borrow_mut(); + + let component = if let Some(dirichlet_bc) = storage.get_component_mut(self.entity) { + let current_bc = mem::replace(&mut dirichlet_bc.bc, Box::new(Empty)); + let union = Union::try_new(vec![current_bc, new_bc]).expect("We expect BCs to be disjoint at construction"); + DirichletBoundaryConditionComponent::from(union) + } else { + DirichletBoundaryConditionComponent::from(new_bc) + }; + storage.insert_component(self.entity, component); + self + } + + /// Set the integrator used for time integration of the model + pub fn set_integrator(&self, integrator: IntegrationMethod) -> Result<&Self, Box> { + let mut fe_models = self + .state + .get_component_storage::() + .borrow_mut(); + + if let Some(model) = fe_models.get_component_mut(self.entity) { + model.integrator = integrator; + + Ok(self) + } else { + Err(Box::from( + "Cannot set integrator: Entity does not have a finite element model yet.", + )) + } + } + + pub fn set_gravity_enabled(&self, enabled: bool) -> Result<&Self, Box> { + let mut fe_models = self + .state + .get_component_storage::() + .borrow_mut(); + + if let Some(model) = fe_models.get_component_mut(self.entity) { + model.gravity_enabled = enabled; + Ok(self) + } else { + Err(Box::from( + "Cannot set gravity: Entity does not have a finite element model yet.", + )) + } + } + + pub fn add_material_surface(&self, material_surface: impl Into) -> Result<&Self, Box> { + let mut fe_models = self + .state + .get_component_storage::() + .borrow_mut(); + let mut surface_meshes = self + .state + .get_component_storage::() + .borrow_mut(); + + if let Some(mut fe_model) = fe_models.get_component_mut(self.entity) { + let material_surface = material_surface.into(); + let interpolator = match_on_finite_element_model_2d!(fe_model.model, model => { + model.make_interpolator(material_surface.vertices())? + }); + + if surface_meshes.get_component(self.entity).is_some() { + panic!("Cannot add surface mesh: Entity already has a surface mesh attached."); + } + + let closed_surface_mesh = ClosedSurfaceMesh2d::from_mesh(material_surface.0.clone())?; + + fe_model.material_surface = Some(closed_surface_mesh); + fe_model.material_surface_interpolator = Some(interpolator); + + surface_meshes.insert(self.entity, material_surface); + + Ok(self) + } else { + panic!( + "No finite element model found for entity. Cannot add material surface.\ + Please add a finite element model to the entity first." + ); + } + } + + /// Sets the boundary conditions of this entity to homogeneous Dirichlet conditions over the given nodes + pub fn set_static_nodes(&self, static_nodes: Vec) -> &Self { + let mut bc_storage = self + .state + .get_component_storage::() + .borrow_mut(); + + bc_storage.insert(self.entity, Homogeneous::new_2d(static_nodes.as_slice()).into()); + + self + } + + pub fn add_name(&self, name: impl Into) -> &Self { + let mut names = self.state.get_component_storage::().borrow_mut(); + + if names.get_component(self.entity).is_some() { + panic!("Cannot add name: Entity already has name."); + } else { + names.insert(self.entity, name.into()); + self + } + } + + pub fn set_material(&self, material: impl Into) -> &Self { + let mut fe_models = self + .state + .get_component_storage::() + .borrow_mut(); + + if let Some(mut fe_model) = fe_models.get_component_mut(self.entity) { + fe_model.material = material.into(); + self + } else { + panic!( + "No finite element model found for entity. Cannot add material.\ + Please add a finite element model to the entity first." + ); + } + } +} + +/// Helper struct to reduce boilerplate when initializing bodies. +pub struct BodyInitializer3d<'a> { + state: &'a StorageContainer, + entity: Entity, +} + +#[allow(dead_code)] +impl<'a> BodyInitializer3d<'a> { + pub fn from_entity(entity: Entity, state: &'a StorageContainer) -> Self { + Self { state, entity } + } + + pub fn initialize_in_state(state: &'a StorageContainer) -> Self { + // Register components as a convenience. Double-registering is unproblematic. + register_known_components().expect("Failed to register components. Should not happen"); + Self { + state, + entity: Entity::new(), + } + } + + pub fn entity(&self) -> Entity { + self.entity + } + + /// Sets the displacement components for all dofs of the attached finite element model + pub fn set_displacement(&self, displacement: &DVector) -> Result<&Self, Box> { + let mut fe_models = self + .state + .get_component_storage::() + .borrow_mut(); + + if let Some(model) = fe_models.get_component_mut(self.entity) { + if displacement.len() == model.u.len() { + model.u = displacement.clone(); + } else { + return Err(Box::from( + "Cannot set displacement: Supplied displacement vector does not have the right dimensions.", + )); + } + + Ok(self) + } else { + Err(Box::from( + "Cannot set displacement: Entity does not have a finite element model yet.", + )) + } + } + + /// Sets the displacements of all nodes of the finite element model to the same value. + pub fn set_uniform_displacement(&self, displacement: &Vector3) -> Result<&Self, Box> { + let num_vertices = self + .state + .get_component_storage::() + .borrow() + .get_component(self.entity) + .ok_or_else(|| "Cannot set displacement without a FE model.")? + .model + .ndof() + / 3; + if num_vertices > 0 { + self.set_displacement(&flatten_vertically(&vec![*displacement; num_vertices]).unwrap())?; + } + + Ok(self) + } + + /// Sets the velocity components for all dofs of the attached finite element model + pub fn set_velocity(&self, velocity: &DVector) -> Result<&Self, Box> { + let mut fe_models = self + .state + .get_component_storage::() + .borrow_mut(); + + if let Some(model) = fe_models.get_component_mut(self.entity) { + if velocity.len() == model.v.len() { + model.v = velocity.clone(); + } else { + return Err(Box::from( + "Cannot set velocity: Supplied velocity vector does not have the right dimensions.", + )); + } + + Ok(self) + } else { + Err(Box::from( + "Cannot set velocity: Entity does not have a finite element model yet.", + )) + } + } + + /// Adds an elastic finite element model and a volume mesh component to the entity. + pub fn add_finite_element_model( + &self, + fe_model: impl Into, + material_volume_mesh: impl Into, + ) -> Result<&Self, Box> { + let mut fe_models = self + .state + .get_component_storage::() + .borrow_mut(); + let mut volume_meshes = self + .state + .get_component_storage::() + .borrow_mut(); + + if fe_models.get_component(self.entity).is_some() { + panic!("Cannot add finite elment model: Entity already has finite element model."); + } else { + let fe_model = fe_model.into(); + let material_volume_mesh = material_volume_mesh.into(); + let ndof = fe_model.ndof(); + + let mesh_interpolator = match_on_finite_element_model_3d!(fe_model, fe_model => { + let accelerator = GeometryCollectionAccelerator::new(fe_model); + FiniteElementInterpolator::interpolate_space( + &accelerator, + material_volume_mesh.vertices())? + }); + + let model = FiniteElementElasticModel3d { + model: fe_model, + u: DVector::zeros(ndof), + v: DVector::zeros(ndof), + factorization: None, + material_volume_mesh: material_volume_mesh.clone(), + material_volume_interpolator: mesh_interpolator, + material: Material::default(), + integrator: Default::default(), + gravity_enabled: true, + rotations: None, + model_matrix_storage: None, + }; + + fe_models.insert(self.entity, model); + volume_meshes.insert(self.entity, material_volume_mesh); + + Ok(self) + } + } + + /// Set the integrator used for time integration of the model + pub fn set_integrator(&self, integrator: IntegrationMethod) -> Result<&Self, Box> { + let mut fe_models = self + .state + .get_component_storage::() + .borrow_mut(); + + if let Some(model) = fe_models.get_component_mut(self.entity) { + model.integrator = integrator; + + Ok(self) + } else { + Err(Box::from( + "Cannot set integrator: Entity does not have a finite element model yet.", + )) + } + } + + pub fn set_gravity_enabled(&self, enabled: bool) -> Result<&Self, Box> { + let mut fe_models = self + .state + .get_component_storage::() + .borrow_mut(); + + if let Some(model) = fe_models.get_component_mut(self.entity) { + model.gravity_enabled = enabled; + Ok(self) + } else { + Err(Box::from( + "Cannot set gravity: Entity does not have a finite element model yet.", + )) + } + } + + /// Sets the boundary conditions of this entity to homogeneous Dirichlet conditions over the given nodes + pub fn set_static_nodes(&self, static_nodes: Vec) -> &Self { + let mut bc_storage = self + .state + .get_component_storage::() + .borrow_mut(); + + bc_storage.insert(self.entity, Homogeneous::new_3d(static_nodes.as_slice()).into()); + + self + } + + pub fn add_name(&self, name: impl Into) -> &Self { + let mut names = self.state.get_component_storage::().borrow_mut(); + + if names.get_component(self.entity).is_some() { + panic!("Cannot add name: Entity already has name."); + } else { + names.insert(self.entity, name.into()); + self + } + } + + pub fn set_material(&self, material: impl Into) -> &Self { + let mut fe_models = self + .state + .get_component_storage::() + .borrow_mut(); + + if let Some(mut fe_model) = fe_models.get_component_mut(self.entity) { + fe_model.material = material.into(); + self + } else { + panic!( + "No finite element model found for entity. Cannot add material.\ + Please add a finite element model to the entity first." + ); + } + } + + pub fn add_volume_mesh(&self, volume_mesh: impl Into) -> &Self { + let volume_mesh = volume_mesh.into(); + let mut meshes = self + .state + .get_component_storage::() + .borrow_mut(); + + if meshes.get_component(self.entity).is_some() { + panic!("Entity already has volume mesh."); + } else { + meshes.insert_component(self.entity, volume_mesh); + } + + self + } + + pub fn add_boundary_conditions(&self, new_bc: Box) -> &Self { + let mut storage = self + .state + .get_component_storage::() + .borrow_mut(); + + let component = if let Some(dirichlet_bc) = storage.get_component_mut(self.entity) { + let current_bc = mem::replace(&mut dirichlet_bc.bc, Box::new(Empty)); + let union = Union::try_new(vec![current_bc, new_bc]).expect("We expect BCs to be disjoint at construction"); + DirichletBoundaryConditionComponent::from(union) + } else { + DirichletBoundaryConditionComponent::from(new_bc) + }; + storage.insert_component(self.entity, component); + self + } +} diff --git a/scene_runner/src/scenes/hollow_ball.rs b/scene_runner/src/scenes/hollow_ball.rs new file mode 100644 index 0000000..829d0f7 --- /dev/null +++ b/scene_runner/src/scenes/hollow_ball.rs @@ -0,0 +1,516 @@ +use std::error::Error; + +use crate::scenes::{filtered_vertex_indices, Scene, SceneConstructor, SceneParameters}; + +use simulation_toolbox::fem::{ + DirichletBoundaryConditionComponent, DirichletBoundaryConditions, FiniteElementIntegrator, + FiniteElementMeshDeformer, FiniteElementModel3d, IntegratorSettings, Material, +}; + +use crate::meshes::load_mesh_from_file; +use crate::scenes::helpers::BodyInitializer3d; +use core::fmt; +use fenris::connectivity::{Connectivity, ConnectivityMut}; +use fenris::embedding::{ + embed_mesh_3d, embed_quadrature_3d, embed_quadrature_3d_with_opts, EmbeddedModelBuilder, EmbeddedQuadrature, + QuadratureOptions, StabilizationOptions, +}; +use fenris::geometry::polymesh::PolyMesh3d; +use fenris::geometry::{ConvexPolygon3d, Quad3d}; +use fenris::lp_solvers::GlopSolver; +use fenris::mesh::{Mesh3d, Tet10Mesh, Tet4Mesh}; +use fenris::model::NodalModel; +use fenris::nalgebra::{Point3, Vector3, U3}; +use fenris::nested_vec::NestedVec; +use fenris::quadrature::{ + tet_quadrature_strength_1, tet_quadrature_strength_2, tet_quadrature_strength_3, tet_quadrature_strength_5, + Quadrature, +}; +use fenris::reorder::reorder_mesh_par; +use fenris::rtree::GeometryCollectionAccelerator; +use fenris::solid::materials::{StableNeoHookeanMaterial, YoungPoisson}; +use fenris::space::FiniteElementSpace; +use hamilton::{Component, Entity, StorageContainer, System, Systems}; +use log::info; +use simulation_toolbox::components::{get_simulation_time, PolyMesh3dCollection, PolyMesh3dComponent, TimeStep}; +use simulation_toolbox::fem::bcs::{ + ConstantUniformAngularVelocity, ConstantUniformDisplacement, ConstantUniformVelocity, Union, +}; +use simulation_toolbox::io::obj::load_single_surface_polymesh3d_obj; +use simulation_toolbox::io::ply::dump_polymesh_faces_ply; +use simulation_toolbox::match_on_finite_element_model_3d; + +pub fn scenes() -> Vec { + vec![ + SceneConstructor { + name: "hollow_ball_fem_tet4_coarse".to_string(), + constructor: hollow_ball_fem_tet4_coarse, + }, + SceneConstructor { + name: "hollow_ball_fem_tet4_medium".to_string(), + constructor: hollow_ball_fem_tet4_medium, + }, + SceneConstructor { + name: "hollow_ball_fem_tet4_fine".to_string(), + constructor: hollow_ball_fem_tet4_fine, + }, + SceneConstructor { + name: "hollow_ball_fem_tet10_coarse".to_string(), + constructor: hollow_ball_fem_tet10_coarse, + }, + SceneConstructor { + name: "hollow_ball_fem_tet10_medium".to_string(), + constructor: hollow_ball_fem_tet10_medium, + }, + SceneConstructor { + name: "hollow_ball_fem_tet10_fine".to_string(), + constructor: hollow_ball_fem_tet10_fine, + }, + SceneConstructor { + name: "hollow_ball_embedded_tet4_coarse".to_string(), + constructor: hollow_ball_embedded_tet4_coarse, + }, + SceneConstructor { + name: "hollow_ball_embedded_tet4_medium".to_string(), + constructor: hollow_ball_embedded_tet4_medium, + }, + SceneConstructor { + name: "hollow_ball_embedded_tet10_coarse".to_string(), + constructor: hollow_ball_embedded_tet10_coarse, + }, + SceneConstructor { + name: "hollow_ball_embedded_tet10_medium".to_string(), + constructor: hollow_ball_embedded_tet10_medium, + }, + ] +} + +fn initial_scene(name: &str) -> Scene { + // Use y-axis gravity to simplify working with meshes that are oriented along the y-axis + let mut initial_state = StorageContainer::default(); + // set_gravity(&mut initial_state, Vector3::new(0.0, -9.81, 0.0)); + let dt = 2e-3; + initial_state.replace_storage(::Storage::new(TimeStep(dt))); + Scene { + initial_state, + simulation_systems: Default::default(), + analysis_systems: Default::default(), + duration: 4.50, + name: String::from(name), + } +} + +fn default_material() -> Material { + Material { + density: 1000.0, + mass_damping_coefficient: None, + stiffness_damping_coefficient: Some(0.01), + elastic_model: StableNeoHookeanMaterial::from(YoungPoisson { + young: 1e7, + poisson: 0.48, + }) + .into(), + } +} + +fn add_systems(systems: &mut Systems) { + let integrator_settings = IntegratorSettings::default().set_project_stiffness(false); + systems.add_system(Box::new(FiniteElementIntegrator::with_settings(integrator_settings))); + systems.add_system(Box::new(FiniteElementMeshDeformer)); +} + +fn load_mesh(params: &SceneParameters, filename: &str) -> Result, Box> { + load_mesh_from_file( + ¶ms.asset_dir, + &format!("meshes/hollow_ball/ball2/proper/{}", filename), + ) +} + +fn load_fine_embedded_mesh(params: &SceneParameters) -> Result, Box> { + // TODO: Use the full resolution mesh eventually + load_mesh(params, FINE_MESH_FILENAME) +} + +const COARSE_CAGE_FILENAME: &'static str = "math_ball_nested_cage_7500_corrected.msh"; +const MEDIUM_MESH_FILENAME: &'static str = "math_ball_medium.msh"; +const FINE_MESH_FILENAME: &'static str = "math_ball.msh"; + +/// System that switches out BCs after a given simulation time +#[derive(Debug)] +pub struct BoundaryConditionSwitchSystem { + time_for_switch: f64, + new_bcs: Option>, + entity: Entity, +} + +impl fmt::Display for BoundaryConditionSwitchSystem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "BoundaryConditionSwitchSystem") + } +} + +impl System for BoundaryConditionSwitchSystem { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + let t = get_simulation_time(data); + let mut bcs = data + .get_component_storage::() + .borrow_mut(); + + if let Ok(t) = t { + if t >= self.time_for_switch { + if let Some(new_bcs) = self.new_bcs.take() { + let bc_component = bcs + .get_component_mut(self.entity) + .expect("Entity must be in simulation"); + bc_component.bc = new_bcs; + } + } + } + + Ok(()) + } +} + +struct BoundaryConditions { + // BCs at the beginning of the scene + initial_bc: Box, + // BCs after switch + final_bc: Box, + // Time when switch takes place + switch_time: f64, +} + +fn set_up_boundary_conditions(mesh_vertices: &[Point3]) -> BoundaryConditions { + // Epsilon for classifying nodes as belonging to a constrained surface + let dist_eps = 0.01; + + // Connectors are placed along z-axis, so we identify them by plus and minus + // (above and below z = 0) + let connector_plus_quad = Quad3d::from_vertices([ + Point3::new(-0.2, -0.2, 2.1), + Point3::new(0.2, -0.2, 2.1), + Point3::new(0.2, 0.2, 2.1), + Point3::new(-0.2, 0.2, 2.1), + ]); + let connector_plus_nodes = filtered_vertex_indices(mesh_vertices, |v| { + connector_plus_quad.project_point(v).distance <= dist_eps + }); + + let connector_minus_quad = Quad3d::from_vertices([ + Point3::new(0.2, -0.2, -2.1), + Point3::new(-0.2, -0.2, -2.1), + Point3::new(-0.2, 0.2, -2.1), + Point3::new(0.2, 0.2, -2.1), + ]); + + let connector_minus_nodes = filtered_vertex_indices(mesh_vertices, |v| { + connector_minus_quad.project_point(v).distance <= dist_eps + }); + + let plus_velocity = Vector3::new(0.0, 0.0, -1.0); + let plus_bc = ConstantUniformVelocity::new(&connector_plus_nodes, plus_velocity); + // let minus_bc = ConstantUniformVelocity::new(&connector_minus_nodes, Vector3::new(0.0, 0.0, 0.4)); + + let minus_node_positions = connector_minus_nodes + .iter() + .copied() + .map(|index| mesh_vertices[index]) + .collect(); + let minus_bc = ConstantUniformAngularVelocity::new( + &connector_minus_nodes, + Vector3::new(0.0, 0.0, 0.8), + Point3::new(0.0, 0.0, -2.1), + minus_node_positions, + ); + + let switch_time = 2.5; + let plus_bc_after = ConstantUniformDisplacement::new(&connector_plus_nodes, plus_velocity * switch_time); + + BoundaryConditions { + initial_bc: Box::new(Union::try_new(vec![Box::new(minus_bc.clone()), Box::new(plus_bc.clone())]).unwrap()), + final_bc: Box::new(Union::try_new(vec![Box::new(minus_bc.clone()), Box::new(plus_bc_after.clone())]).unwrap()), + switch_time, + } +} + +fn add_model<'a, Model, C>( + params: &SceneParameters, + scene: &mut Scene, + model: Model, + mesh: &Mesh3d, + _volume_poly_mesh: PolyMesh3d, +) -> Result<(), Box> +where + Model: Into, + C: Connectivity, + C::FaceConnectivity: ConnectivityMut, +{ + let model = model.into(); + + match_on_finite_element_model_3d!(model, model => { + info!("Setting up model. Vertices: {}. Elements: {}", + model.vertices().len(), model.num_connectivities()); + }); + + info!("Generating render mesh"); + let render_mesh_path = params + .asset_dir + .join("meshes/hollow_ball/ball2/proper/math_ball.obj"); + let render_surface_mesh = load_single_surface_polymesh3d_obj(&render_mesh_path)?; + + info!("Generating wireframes"); + let (wireframe_volume, wireframe_surface) = { + let fe_mesh_volume = mesh.extract_face_soup(); + let fe_mesh_surface = mesh.extract_surface_mesh(); + let mut wireframe_volume = PolyMesh3d::from_surface_mesh(&fe_mesh_volume); + let mut wireframe_surface = PolyMesh3d::from_surface_mesh(&fe_mesh_surface); + wireframe_volume.split_edges_n_times(2); + wireframe_surface.split_edges_n_times(2); + + (wireframe_volume, wireframe_surface) + }; + + let render_component = match_on_finite_element_model_3d!(&model, model => { + let accelerator = GeometryCollectionAccelerator::new(model); + PolyMesh3dComponent::new("render", render_surface_mesh) + .with_subfolder("render_meshes") + .with_interpolator(&accelerator)? + }); + + info!("Setting up model"); + let material = default_material(); + let bcs = match_on_finite_element_model_3d!(&model, model => { + set_up_boundary_conditions(model.vertices()) + }); + + // For now we're not interested in exporting the volume meshes as we want to save the space + let volume_poly_mesh = PolyMesh3d::from_poly_data(Vec::new(), NestedVec::new(), NestedVec::new()); + + let entity = BodyInitializer3d::initialize_in_state(&scene.initial_state) + .add_name(scene.name.clone()) + .add_finite_element_model(model, volume_poly_mesh)? + .set_material(material) + .add_boundary_conditions(bcs.initial_bc) + .entity(); + + scene + .simulation_systems + .add_system(Box::new(BoundaryConditionSwitchSystem { + time_for_switch: bcs.switch_time, + new_bcs: Some(bcs.final_bc), + entity, + })); + + scene + .initial_state + .insert_component(entity, PolyMesh3dCollection(vec![render_component])); + + { + // We don't want to export wireframes at every step, only at scene creation + let ply_dir = params.output_dir.join("ply"); + dump_polymesh_faces_ply(&wireframe_surface, &ply_dir, "wireframe_surface.ply")?; + dump_polymesh_faces_ply(&wireframe_volume, &ply_dir, "wireframe_volume.ply")?; + } + + Ok(()) +} + +fn hollow_ball_fem_tet4(params: &SceneParameters, name: &str, filename: &str) -> Result> { + let mut scene = initial_scene(name); + + let tet_mesh = load_mesh(params, filename)?; + let volume_poly_mesh = PolyMesh3d::from(&tet_mesh); + let tet_mesh = reorder_mesh_par(&tet_mesh).apply(&tet_mesh); + + let quadrature = tet_quadrature_strength_1(); + let fe_model = NodalModel::from_mesh_and_quadrature(tet_mesh.clone(), quadrature) + .with_mass_quadrature(tet_quadrature_strength_2()); + add_model(params, &mut scene, fe_model, &tet_mesh, volume_poly_mesh)?; + add_systems(&mut scene.simulation_systems); + Ok(scene) +} + +pub fn hollow_ball_fem_tet4_coarse(params: &SceneParameters) -> Result> { + let name = "hollow_ball_fem_tet4_coarse"; + hollow_ball_fem_tet4(params, &name, COARSE_CAGE_FILENAME) +} + +pub fn hollow_ball_fem_tet4_medium(params: &SceneParameters) -> Result> { + let name = "hollow_ball_fem_tet4_medium"; + hollow_ball_fem_tet4(params, &name, MEDIUM_MESH_FILENAME) +} + +pub fn hollow_ball_fem_tet4_fine(params: &SceneParameters) -> Result> { + let name = "hollow_ball_fem_tet4_fine"; + hollow_ball_fem_tet4(params, &name, FINE_MESH_FILENAME) +} + +fn hollow_ball_fem_tet10(params: &SceneParameters, name: &str, filename: &str) -> Result> { + let mut scene = initial_scene(name); + + let tet_mesh = load_mesh(params, filename)?; + let volume_poly_mesh = PolyMesh3d::from(&tet_mesh); + let tet_mesh = Tet10Mesh::from(&tet_mesh); + let tet_mesh = reorder_mesh_par(&tet_mesh).apply(&tet_mesh); + + let quadrature = tet_quadrature_strength_3(); + let fe_model = NodalModel::from_mesh_and_quadrature(tet_mesh.clone(), quadrature) + .with_mass_quadrature(tet_quadrature_strength_5()); + add_model(params, &mut scene, fe_model, &tet_mesh, volume_poly_mesh)?; + add_systems(&mut scene.simulation_systems); + Ok(scene) +} + +pub fn hollow_ball_fem_tet10_coarse(params: &SceneParameters) -> Result> { + let name = "hollow_ball_fem_tet10_coarse"; + hollow_ball_fem_tet10(params, &name, COARSE_CAGE_FILENAME) +} + +pub fn hollow_ball_fem_tet10_medium(params: &SceneParameters) -> Result> { + let name = "hollow_ball_fem_tet10_medium"; + hollow_ball_fem_tet10(params, &name, MEDIUM_MESH_FILENAME) +} + +pub fn hollow_ball_fem_tet10_fine(params: &SceneParameters) -> Result> { + let name = "hollow_ball_fem_tet10_fine"; + hollow_ball_fem_tet10(params, &name, FINE_MESH_FILENAME) +} + +fn count_interface_points(quadrature: &EmbeddedQuadrature) -> usize { + quadrature + .interface_quadratures() + .iter() + .map(|quadrature| quadrature.points().len()) + .sum::() +} + +fn hollow_ball_embedded_tet10( + params: &SceneParameters, + name: &str, + background_mesh_filename: &str, +) -> Result> { + let mut scene = initial_scene(name); + + let embedded_mesh = load_fine_embedded_mesh(params)?; + let volume_poly_mesh = PolyMesh3d::from(&embedded_mesh); + let background_mesh = load_mesh(params, background_mesh_filename)?; + let background_mesh = Tet10Mesh::from(&background_mesh); + let background_mesh = reorder_mesh_par(&background_mesh).apply(&background_mesh); + + // TODO: Use different stabilization options once we have different quadrature rules for + // mass/stiffness? + info!("Embedding mesh..."); + let embedding = embed_mesh_3d(&background_mesh, &volume_poly_mesh); + let quadrature_opts = QuadratureOptions { + stabilization: Some(StabilizationOptions { + stabilization_factor: 1e-8, + stabilization_quadrature: tet_quadrature_strength_2(), + }), + }; + info!("Constructing mass quadrature..."); + let mass_quadrature = embed_quadrature_3d_with_opts( + &background_mesh, + &embedding, + tet_quadrature_strength_5(), + tet_quadrature_strength_5(), + &quadrature_opts, + )?; + info!( + "Constructed mass quadrature: {} points in interface elements.", + count_interface_points(&mass_quadrature) + ); + info!("Constructing stiffness quadrature..."); + let stiffness_quadrature = embed_quadrature_3d_with_opts( + &background_mesh, + &embedding, + tet_quadrature_strength_3(), + tet_quadrature_strength_3(), + &quadrature_opts, + )?; + info!( + "Constructed stiffness quadrature: {} points in interface elements.", + count_interface_points(&stiffness_quadrature) + ); + info!("Simplifying stiffness quadrature..."); + let stiffness_quadrature = stiffness_quadrature.simplified(3, &GlopSolver::new())?; + info!( + "Simplified stiffness quadrature: {} points in interface elements", + count_interface_points(&stiffness_quadrature) + ); + let elliptic_quadrature = stiffness_quadrature.clone(); + + let fe_model = EmbeddedModelBuilder::from_embedding(&background_mesh, embedding) + .mass_quadrature(mass_quadrature) + .stiffness_quadrature(stiffness_quadrature) + .elliptic_quadrature(elliptic_quadrature) + .build(); + + add_model(params, &mut scene, fe_model, &background_mesh, volume_poly_mesh)?; + add_systems(&mut scene.simulation_systems); + Ok(scene) +} + +pub fn hollow_ball_embedded_tet10_coarse(params: &SceneParameters) -> Result> { + let name = "hollow_ball_embedded_tet10_coarse"; + hollow_ball_embedded_tet10(params, &name, COARSE_CAGE_FILENAME) +} + +pub fn hollow_ball_embedded_tet10_medium(params: &SceneParameters) -> Result> { + let name = "hollow_ball_embedded_tet10_medium"; + hollow_ball_embedded_tet10(params, &name, MEDIUM_MESH_FILENAME) +} + +fn hollow_ball_embedded_tet4( + params: &SceneParameters, + name: &str, + background_mesh_filename: &str, +) -> Result> { + let mut scene = initial_scene(name); + + let embedded_mesh = load_fine_embedded_mesh(params)?; + let volume_poly_mesh = PolyMesh3d::from(&embedded_mesh); + let background_mesh = load_mesh(params, background_mesh_filename)?; + let background_mesh = reorder_mesh_par(&background_mesh).apply(&background_mesh); + + let embedding = embed_mesh_3d(&background_mesh, &volume_poly_mesh); + + // TODO: We don't use any stabilization because that should not be necessary for linear + // tet elements. Is this correct? + + let mass_quadrature = embed_quadrature_3d( + &background_mesh, + &embedding, + tet_quadrature_strength_2(), + tet_quadrature_strength_2(), + )?; + + let stiffness_quadrature = embed_quadrature_3d( + &background_mesh, + &embedding, + tet_quadrature_strength_1(), + tet_quadrature_strength_1(), + )? + // We want to ensure that we obtain a single-point zeroth-order quadrature + .simplified(0, &GlopSolver::new())?; + let elliptic_quadrature = stiffness_quadrature.clone(); + + let fe_model = EmbeddedModelBuilder::from_embedding(&background_mesh, embedding) + .mass_quadrature(mass_quadrature) + .stiffness_quadrature(stiffness_quadrature) + .elliptic_quadrature(elliptic_quadrature) + .build(); + + add_model(params, &mut scene, fe_model, &background_mesh, volume_poly_mesh)?; + add_systems(&mut scene.simulation_systems); + Ok(scene) +} + +pub fn hollow_ball_embedded_tet4_coarse(params: &SceneParameters) -> Result> { + let name = "hollow_ball_embedded_tet4_coarse"; + hollow_ball_embedded_tet4(params, &name, COARSE_CAGE_FILENAME) +} + +pub fn hollow_ball_embedded_tet4_medium(params: &SceneParameters) -> Result> { + let name = "hollow_ball_embedded_tet4_medium"; + hollow_ball_embedded_tet4(params, &name, MEDIUM_MESH_FILENAME) +} diff --git a/scene_runner/src/scenes/mod.rs b/scene_runner/src/scenes/mod.rs new file mode 100644 index 0000000..8eb3207 --- /dev/null +++ b/scene_runner/src/scenes/mod.rs @@ -0,0 +1,114 @@ +use std::error::Error; +use std::path::PathBuf; + +use fenris::nalgebra::allocator::Allocator; +use fenris::nalgebra::{DefaultAllocator, DimName, Point, RealField}; +use hamilton::{StorageContainer, Systems}; +use once_cell::sync::Lazy; +use simulation_toolbox::io::json_helper::JsonWrapper; + +// Single scenes +mod cantilever3d; +mod hollow_ball; +mod rotating_bicycle; + +// Multiple scene modules +mod armadillo_slingshot; +mod cylinder_shell; + +// Special scenes +mod quad_reduc; + +// Other modules +mod helpers; + +static SCENE_REGISTRY: Lazy> = Lazy::new(|| { + let mut scenes = Vec::new(); + scenes.push(SceneConstructor { + name: "cantilever3d".to_string(), + constructor: cantilever3d::cantilever3d, + }); + scenes.push(SceneConstructor { + name: "bicycle_embedded_super_coarse".to_string(), + constructor: rotating_bicycle::build_bicycle_scene_embedded_super_coarse, + }); + scenes.push(SceneConstructor { + name: "bicycle_embedded_coarse".to_string(), + constructor: rotating_bicycle::build_bicycle_scene_embedded_coarse, + }); + scenes.push(SceneConstructor { + name: "bicycle_embedded_fine".to_string(), + constructor: rotating_bicycle::build_bicycle_scene_embedded_fine, + }); + scenes.push(SceneConstructor { + name: "bicycle_fem_coarse".to_string(), + constructor: rotating_bicycle::build_bicycle_scene_fem_coarse, + }); + scenes.push(SceneConstructor { + name: "bicycle_fem_fine".to_string(), + constructor: rotating_bicycle::build_bicycle_scene_fem_fine, + }); + + scenes.extend(cylinder_shell::scenes()); + scenes.extend(armadillo_slingshot::scenes()); + scenes.extend(hollow_ball::scenes()); + scenes.extend(quad_reduc::scenes()); + + scenes.sort_by_key(|constructor| constructor.name.clone()); + scenes +}); + +#[derive(Debug)] +pub struct Scene { + pub initial_state: StorageContainer, + pub simulation_systems: Systems, + pub analysis_systems: Systems, + pub name: String, + pub duration: f64, +} + +#[derive(Debug, Clone)] +pub struct SceneParameters { + pub output_dir: PathBuf, + pub asset_dir: PathBuf, + pub config_file: Option>, +} + +#[doc(hidden)] +pub struct SceneConstructor { + name: String, + constructor: fn(&SceneParameters) -> Result>, +} + +pub fn available_scenes() -> Vec { + let mut names = Vec::new(); + for scene in SCENE_REGISTRY.iter() { + names.push(scene.name.clone()); + } + names +} + +pub fn load_scene(name: &str, params: &SceneParameters) -> Result> { + for scene in SCENE_REGISTRY.iter() { + if scene.name == name { + return (scene.constructor)(params); + } + } + + Err(Box::from(format!("Could not find scene {}", name))) +} + +fn filtered_vertex_indices(vertices: &[Point], filter: F) -> Vec +where + T: RealField, + D: DimName, + F: Fn(&Point) -> bool, + DefaultAllocator: Allocator, +{ + vertices + .iter() + .enumerate() + .filter(|(_, v)| filter(v)) + .map(|(i, _)| i) + .collect() +} diff --git a/scene_runner/src/scenes/quad_reduc.rs b/scene_runner/src/scenes/quad_reduc.rs new file mode 100644 index 0000000..9dd6e9e --- /dev/null +++ b/scene_runner/src/scenes/quad_reduc.rs @@ -0,0 +1,557 @@ +use std::error::Error; +use std::fs; +use std::io::Write; +use std::path::Path; + +use fenris::element::ElementConnectivity; +use fenris::embedding::{compute_element_embedded_quadrature, embed_mesh_3d, optimize_quadrature, QuadratureOptions}; +use fenris::geometry::polymesh::PolyMesh3d; +use fenris::geometry::procedural::create_rectangular_uniform_hex_mesh; +use fenris::geometry::vtk::write_vtk; +use fenris::geometry::ConvexPolyhedron; +use fenris::lp_solvers::GlopSolver; +use fenris::nalgebra::{Point3, Rotation3, Unit, Vector3}; +use fenris::quadrature::{ + tet_quadrature_strength_1, tet_quadrature_strength_10, tet_quadrature_strength_2, tet_quadrature_strength_3, + tet_quadrature_strength_5, Quadrature, +}; +use fenris::vtkio::model::DataSet; + +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use simulation_toolbox::io::json_helper::serde_json; + +use crate::scenes::helpers::PointHelper; +use crate::scenes::{Scene, SceneConstructor, SceneParameters}; + +pub fn scenes() -> Vec { + vec![ + SceneConstructor { + name: "quad_reduc_monomials".to_string(), + constructor: build_quad_reduc_monomials, + }, + SceneConstructor { + name: "quad_reduc_box".to_string(), + constructor: build_quad_reduc_box, + }, + ] +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct MonomialResultSet { + strength: usize, + results: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct MonomialResult { + exponents: [u16; 3], + result: QuadratureReduction, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct QuadratureReduction { + original_strength: usize, + reduced_strength: usize, + original_points: usize, + reduced_points: usize, + exact_integral: f64, + original_integral: f64, + original_abs_error: f64, + original_rel_error: f64, + reduced_integral: f64, + reduced_abs_error: f64, + reduced_rel_error: f64, +} + +#[derive(Clone, Debug)] +struct PolynomialTerm { + exponents: Vector3, + coefficient: f64, +} + +impl PolynomialTerm { + fn new_axyz(a: f64, x_exp: u16, y_exp: u16, z_exp: u16) -> Self { + PolynomialTerm { + exponents: Vector3::new(x_exp as i32, y_exp as i32, z_exp as i32), + coefficient: a, + } + } + + fn evaluate(&self, x: &Vector3) -> f64 { + let mut y = self.coefficient; + for i in 0..3 { + y *= x[i].powi(self.exponents[i] as i32); + } + y + } + + fn indefinite_integral(&self, x: &Vector3) -> f64 { + let e = &self.exponents; + let numerator = self.coefficient * x.x.powi(e.x + 1) * x.y.powi(e.y + 1) * x.z.powi(e.z + 1); + let denominator = ((e.z + 1) * (e.x * e.y + e.x + e.y + 1)) as f64; + numerator / denominator + } + + #[rustfmt::skip] + fn definite_integral(&self, x_min: &Vector3, x_max: &Vector3) -> f64 { + let f = |x| self.indefinite_integral(x); + + let points = [ + Vector3::new(x_max.x, x_max.y, x_max.z), + Vector3::new(x_min.x, x_max.y, x_max.z), + Vector3::new(x_max.x, x_min.y, x_max.z), + Vector3::new(x_max.x, x_max.y, x_min.z), + Vector3::new(x_min.x, x_min.y, x_max.z), + Vector3::new(x_min.x, x_max.y, x_min.z), + Vector3::new(x_max.x, x_min.y, x_min.z), + Vector3::new(x_min.x, x_min.y, x_min.z), + ]; + + let result = + f(&points[0]) - f(&points[1]) + - f(&points[2]) - f(&points[3]) + + f(&points[4]) + f(&points[5]) + + f(&points[6]) - f(&points[7]); + + result + } +} + +#[derive(Clone, Debug)] +struct Polynomial { + terms: Vec, +} + +impl Polynomial { + fn evaluate(&self, x: &Vector3) -> f64 { + let mut y = 0.0; + for term in &self.terms { + y += term.evaluate(x); + } + y + } + + fn definite_integral(&self, x_min: &Vector3, x_max: &Vector3) -> f64 { + let mut y = 0.0; + for term in &self.terms { + y += term.definite_integral(x_min, x_max); + } + y + } +} + +pub fn rotate_deg(points: &mut [Point3], angle_in_deg: f64, axis: &Unit>) { + let angle = angle_in_deg * 180.0 * std::f64::consts::PI.recip(); + let rot = Rotation3::from_axis_angle(axis, angle); + for p in points { + *p = rot * p.clone(); + } +} + +fn write_mesh_to_vtk, S: AsRef, D: Into>( + base_path: P, + name: S, + dataset: D, +) -> Result<(), Box> { + let filename = base_path + .as_ref() + .join(format!("{name}.vtk", name = name.as_ref())); + Ok(write_vtk(dataset.into(), filename, name.as_ref())?) +} + +fn build_quad_reduc_monomials(params: &SceneParameters) -> Result> { + let scene = Scene { + initial_state: Default::default(), + simulation_systems: Default::default(), + analysis_systems: Default::default(), + duration: 0.0, + name: String::from("quad_reduc_monomials"), + }; + + let embedded_mesh_resolution = 1; + let embedded_box_size = 0.5; + let embedded_box_rotation_angle_deg = 32.0; + let embedded_box_rotation_axis = Unit::new_normalize(Vector3::new(1.0, 0.8, 0.9)); + + let embedded_box_rotation_angle_rad = embedded_box_rotation_angle_deg * 180.0 * std::f64::consts::PI.recip(); + + let mut embedded_mesh = create_rectangular_uniform_hex_mesh(1.0, 1, 1, 1, embedded_mesh_resolution); + PointHelper::scale_max_extent_to(embedded_mesh.vertices_mut(), embedded_box_size); + PointHelper::center_to_origin(embedded_mesh.vertices_mut()); + + let embedded_bb = PointHelper::bb(embedded_mesh.vertices()).unwrap(); + let embedded_volume = embedded_mesh + .cell_iter() + .map(|tet| tet.compute_volume()) + .fold(0.0, std::ops::Add::add); + println!("Volume of embedded box: {}", embedded_volume); + + // Rotate the mesh + rotate_deg( + embedded_mesh.vertices_mut(), + embedded_box_rotation_angle_deg, + &embedded_box_rotation_axis, + ); + + let embedded_displacement = { + // Find min point of mesh + let aabb = PointHelper::bb(embedded_mesh.vertices()).unwrap(); + let min = aabb.min(); + + let safety = Vector3::repeat(1e-1 * embedded_box_size); + let embedded_displacement = -(*min - safety); + + // Displace it into first octant + for v in embedded_mesh.vertices_mut() { + v.coords += embedded_displacement; + } + + embedded_displacement + }; + + let mut background_mesh = create_rectangular_uniform_hex_mesh(2.0, 1, 1, 1, 1); + PointHelper::center_to_origin(background_mesh.vertices_mut()); + + write_mesh_to_vtk(¶ms.output_dir, "embedded_mesh", &embedded_mesh)?; + write_mesh_to_vtk(¶ms.output_dir, "background_mesh", &background_mesh)?; + + let embedded_mesh = PolyMesh3d::from(&embedded_mesh); + let embedding = embed_mesh_3d(&background_mesh, &embedded_mesh); + + // Define function f to integrate, computed by rotating points into the coordinate + // system of the embedded box. + let construct_f = |poly: &Polynomial| { + let p = poly.clone(); + move |x: &Vector3| -> f64 { + let x = x - &embedded_displacement; + let rot = Rotation3::from_axis_angle(&embedded_box_rotation_axis, -embedded_box_rotation_angle_rad); + let x_transformed = rot * x + &embedded_displacement; + p.evaluate(&x_transformed) + } + }; + + // Quadrature strengths and rules + let tet_quadratures = vec![ + (1, tet_quadrature_strength_1(), 1), + (2, tet_quadrature_strength_2(), 2), + (3, tet_quadrature_strength_3(), 3), + (4, tet_quadrature_strength_5(), 5), + (5, tet_quadrature_strength_5(), 5), + (6, tet_quadrature_strength_10(), 10), + (7, tet_quadrature_strength_10(), 10), + (8, tet_quadrature_strength_10(), 10), + (9, tet_quadrature_strength_10(), 10), + (10, tet_quadrature_strength_10(), 10), + ]; + + let construct_monomials_exponents = |order: usize| -> Vec<[u16; 3]> { + std::iter::repeat(0..=order) + // We want a polynomial in three dimensions, so take 3x [0..N] + .take(3) + .multi_cartesian_product() + // Filter out all terms with lower order + .filter(|exponents| exponents.iter().sum::() <= order) + .map(|exponents| { + assert!(exponents.len() == 3); + [exponents[0] as u16, exponents[1] as u16, exponents[2] as u16] + }) + .collect() + }; + + let mut results = Vec::new(); + for (strength, quadrature, original_strength) in tet_quadratures { + println!( + "Original strength: {}, reduced strength: {}", + original_strength, strength + ); + + let quadratures: Vec<_> = embedding + .interface_cells + .iter() + .zip(embedding.interface_cell_embeddings.iter()) + .map(|(bg_cell_idx, embedded_intersection)| { + let element = background_mesh + .connectivity() + .get(*bg_cell_idx) + .unwrap() + .element(background_mesh.vertices()) + .unwrap(); + compute_element_embedded_quadrature( + &element, + embedded_intersection, + &quadrature, + &QuadratureOptions::default(), + ) + .unwrap() + }) + .collect(); + + assert_eq!(quadratures.len(), 1); + println!("Computed quadrature. Starting optimization..."); + + let quadrature = quadratures.first().unwrap(); + let quadrature_opt = optimize_quadrature(&quadrature, strength, &GlopSolver::new()).unwrap(); + + println!( + "Num quadrature points before optimization: {}", + quadrature.points().len() + ); + println!( + "Num quadrature points after optimization: {}", + quadrature_opt.points().len() + ); + + let exponent_sets = construct_monomials_exponents(strength); + let mut monomial_results = Vec::with_capacity(exponent_sets.len()); + + println!("Monomial exponent sets for strength {}:", strength); + for exponents in exponent_sets.iter() { + println!("{:?}", exponents); + } + + println!("Computing integrals..."); + for exponents in exponent_sets.iter() { + let monomial = Polynomial { + terms: vec![PolynomialTerm::new_axyz(1.0, exponents[0], exponents[1], exponents[2])], + }; + + let f = construct_f(&monomial); + + let exact_integral: f64 = monomial.definite_integral( + &(embedded_bb.min() + embedded_displacement), + &(embedded_bb.max() + embedded_displacement), + ); + //monomial.definite_integral(embedded_bb.min(), embedded_bb.max()); + let original_integral: f64 = quadrature.integrate(|x| f(x)); + let optimized_integral: f64 = quadrature_opt.integrate(|x| f(x)); + + let original_absdiff = (exact_integral - original_integral).abs(); + let optimized_absdiff = (exact_integral - optimized_integral).abs(); + + let (original_reldiff, optimized_reldiff) = { + let original_reldiff = original_absdiff / exact_integral.abs(); + let optimized_reldiff = optimized_absdiff / exact_integral.abs(); + + //assert!(original_reldiff.is_finite()); + //assert!(optimized_reldiff.is_finite()); + + (original_reldiff, optimized_reldiff) + }; + + println!("Exact integral : {:.15e}", exact_integral); + println!("Original integral : {:.15e}", original_integral); + println!("Original abs error : {:.3e}", original_absdiff); + println!("Original rel error : {:.3e}", original_reldiff); + println!("Optimized integral : {:.15e}", optimized_integral); + println!("Optimized abs error: {:.3e}", optimized_absdiff); + println!("Optimized rel error: {:.3e}", optimized_reldiff); + + monomial_results.push(MonomialResult { + exponents: exponents.clone(), + result: QuadratureReduction { + original_strength, + reduced_strength: strength, + original_points: quadrature.points().len(), + reduced_points: quadrature_opt.points().len(), + exact_integral: exact_integral, + original_integral: original_integral, + original_abs_error: original_absdiff, + original_rel_error: original_reldiff, + reduced_integral: optimized_integral, + reduced_abs_error: optimized_absdiff, + reduced_rel_error: optimized_reldiff, + }, + }); + + println!(""); + } + + results.push(MonomialResultSet { + strength, + results: monomial_results, + }) + } + + let output_json = serde_json::to_string_pretty(&results)?; + let mut json_file = fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(r#"U:\Documents\Programming\rust\femproto2\notebooks\quad_plots\quad_reduc_results_monomials.json"#)?; + json_file.write_all(output_json.as_bytes())?; + + Ok(scene) +} + +fn build_quad_reduc_box(params: &SceneParameters) -> Result> { + let scene = Scene { + initial_state: Default::default(), + simulation_systems: Default::default(), + analysis_systems: Default::default(), + duration: 0.0, + name: String::from("quad_reduc_box"), + }; + + let embedded_mesh_resolution = 1; + let embedded_box_size = 1.1; + let embedded_box_rotation_angle_deg = 32.0; + let embedded_box_rotation_axis = Unit::new_normalize(Vector3::new(1.0, 0.8, 0.9)); + let embedded_box_rotation_angle_rad = embedded_box_rotation_angle_deg * 180.0 * std::f64::consts::PI.recip(); + + let mut embedded_mesh = create_rectangular_uniform_hex_mesh(1.0, 1, 1, 1, embedded_mesh_resolution); + PointHelper::scale_max_extent_to(embedded_mesh.vertices_mut(), embedded_box_size); + PointHelper::center_to_origin(embedded_mesh.vertices_mut()); + + let embedded_bb = PointHelper::bb(embedded_mesh.vertices()).unwrap(); + let embedded_volume = embedded_mesh + .cell_iter() + .map(|tet| tet.compute_volume()) + .fold(0.0, std::ops::Add::add); + println!("Volume of embedded box: {}", embedded_volume); + + rotate_deg( + embedded_mesh.vertices_mut(), + embedded_box_rotation_angle_deg, + &embedded_box_rotation_axis, + ); + + let mut background_mesh = create_rectangular_uniform_hex_mesh(2.0, 1, 1, 1, 1); + PointHelper::center_to_origin(background_mesh.vertices_mut()); + + write_mesh_to_vtk(¶ms.output_dir, "embedded_mesh", &embedded_mesh)?; + write_mesh_to_vtk(¶ms.output_dir, "background_mesh", &background_mesh)?; + + let embedded_mesh = PolyMesh3d::from(&embedded_mesh); + let embedding = embed_mesh_3d(&background_mesh, &embedded_mesh); + + // Polynomial defined in non-rotated embedded object + let poly = Polynomial { + terms: vec![ + PolynomialTerm::new_axyz(0.05, 0, 0, 0), + PolynomialTerm::new_axyz(0.1, 0, 1, 0), + PolynomialTerm::new_axyz(1.0, 0, 0, 1), + PolynomialTerm::new_axyz(1.0, 2, 0, 0), + PolynomialTerm::new_axyz(1.0, 0, 2, 0), + PolynomialTerm::new_axyz(10.0, 2, 2, 2), + PolynomialTerm::new_axyz(3.0, 4, 2, 1), + PolynomialTerm::new_axyz(20.5, 6, 0, 4), + PolynomialTerm::new_axyz(-1.5, 2, 2, 1), + PolynomialTerm::new_axyz(30.0, 4, 2, 2), + PolynomialTerm::new_axyz(200.0, 2, 5, 3), + PolynomialTerm::new_axyz(3000.0, 7, 2, 1), + PolynomialTerm::new_axyz(100.0, 9, 0, 0), + PolynomialTerm::new_axyz(1.0, 0, 0, 9), + ], + }; + + // Define function f to integrate, computed by rotating points into the coordinate + // system of the embedded box. + let f = { + let p = poly.clone(); + move |x: &Vector3| -> f64 { + let rot = Rotation3::from_axis_angle(&embedded_box_rotation_axis, -embedded_box_rotation_angle_rad); + let x_transformed = rot * x; + p.evaluate(&x_transformed) + } + }; + + // Quadrature strengths and rules + let tet_quadratures = vec![ + (1, tet_quadrature_strength_1(), 1), + (2, tet_quadrature_strength_2(), 2), + (3, tet_quadrature_strength_3(), 3), + (4, tet_quadrature_strength_5(), 5), + (5, tet_quadrature_strength_5(), 5), + (6, tet_quadrature_strength_10(), 10), + (7, tet_quadrature_strength_10(), 10), + (8, tet_quadrature_strength_10(), 10), + (9, tet_quadrature_strength_10(), 10), + (10, tet_quadrature_strength_10(), 10), + ]; + + let mut output = Vec::new(); + for (strength, quadrature, original_strength) in tet_quadratures { + println!( + "Original strength: {}, reduced strength: {}", + original_strength, strength + ); + + let quadratures: Vec<_> = embedding + .interface_cells + .iter() + .zip(embedding.interface_cell_embeddings.iter()) + .map(|(bg_cell_idx, embedded_intersection)| { + let element = background_mesh + .connectivity() + .get(*bg_cell_idx) + .unwrap() + .element(background_mesh.vertices()) + .unwrap(); + compute_element_embedded_quadrature( + &element, + embedded_intersection, + &quadrature, + &QuadratureOptions::default(), + ) + .unwrap() + }) + .collect(); + + assert_eq!(quadratures.len(), 1); + println!("Computed quadrature. Starting optimization..."); + + let quadrature = quadratures.first().unwrap(); + let quadrature_opt = optimize_quadrature(&quadrature, strength, &GlopSolver::new()).unwrap(); + + println!( + "Num quadrature points before optimization: {}", + quadrature.points().len() + ); + println!( + "Num quadrature points after optimization: {}", + quadrature_opt.points().len() + ); + + let exact_integral: f64 = poly.definite_integral(embedded_bb.min(), embedded_bb.max()); + let original_integral: f64 = quadrature.integrate(|x| f(x)); + let optimized_integral: f64 = quadrature_opt.integrate(|x| f(x)); + + let original_absdiff = (exact_integral - original_integral).abs(); + let optimized_absdiff = (exact_integral - optimized_integral).abs(); + let original_reldiff = original_absdiff / exact_integral.abs(); + let optimized_reldiff = optimized_absdiff / exact_integral.abs(); + + println!("Exact integral : {:.15e}", exact_integral); + println!("Original integral : {:.15e}", original_integral); + println!("Original abs error : {:.3e}", original_absdiff); + println!("Original rel error : {:.3e}", original_reldiff); + println!("Optimized integral : {:.15e}", optimized_integral); + println!("Optimized abs error: {:.3e}", optimized_absdiff); + println!("Optimized rel error: {:.3e}", optimized_reldiff); + + output.push(QuadratureReduction { + original_strength, + reduced_strength: strength, + original_points: quadrature.points().len(), + reduced_points: quadrature_opt.points().len(), + exact_integral: exact_integral, + original_integral: original_integral, + original_abs_error: original_absdiff, + original_rel_error: original_reldiff, + reduced_integral: optimized_integral, + reduced_abs_error: optimized_absdiff, + reduced_rel_error: optimized_reldiff, + }) + } + + let output_json = serde_json::to_string_pretty(&output)?; + let mut json_file = fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(r#"U:\Documents\Programming\rust\femproto2\notebooks\quad_plots\quad_reduc_results.json"#)?; + json_file.write_all(output_json.as_bytes())?; + + Ok(scene) +} diff --git a/scene_runner/src/scenes/rotating_bicycle.rs b/scene_runner/src/scenes/rotating_bicycle.rs new file mode 100644 index 0000000..89b0246 --- /dev/null +++ b/scene_runner/src/scenes/rotating_bicycle.rs @@ -0,0 +1,262 @@ +use std::error::Error; + +use fenris::connectivity::Connectivity; +use fenris::embedding::construct_embedded_model_2d; +use fenris::mesh::{Mesh2d, TriangleMesh2d}; +use fenris::model::NodalModel2d; +use fenris::nalgebra::{DVector, Point2, RealField, Vector2}; +use fenris::quadrature::{quad_quadrature_strength_5_f64, tri_quadrature_strength_5_f64}; +use fenris::solid::materials::{StVKMaterial, YoungPoisson}; +use numeric_literals::replace_float_literals; +use simulation_toolbox::components::{set_gravity, PointInterpolator}; +use simulation_toolbox::fem::{ElasticMaterialModel, FiniteElementIntegrator, FiniteElementMeshDeformer, Material}; + +use crate::meshes; +use crate::scenes::helpers::{ + generate_background_mesh_for_tri2d, generate_single_cell_background_mesh_for_tri2d, BodyInitializer2d, +}; +use crate::scenes::{Scene, SceneParameters}; + +struct BicycleSceneSettings { + /// Name of the scene + name: String, + /// Duration of the scene + duration: f64, + + /// Whether to use the finite cell method (classic FEM otherwise) + use_embedded_model: bool, + /// Whether to use the fine mesh for the embedded/volume mesh + use_fine_mesh: bool, + + /// Density of the bike + density: f64, + /// Young's modulus + young: f64, + /// Poisson ratio + poisson: f64, + /// Resolution of the background mesh used in the finite cell method + background_resolution: Option, + /// Rotational velocity of the bike + omega: f64, +} + +impl Default for BicycleSceneSettings { + fn default() -> Self { + Self { + name: "bicycle".to_string(), + duration: 15.0, + use_embedded_model: true, + use_fine_mesh: true, + density: 100.0, + young: 1e9, + poisson: 0.4, + background_resolution: Some(0.8), + omega: 10.0, + } + } +} + +pub fn build_bicycle_scene_embedded_super_coarse(_params: &SceneParameters) -> Result> { + let mut settings = BicycleSceneSettings::default(); + settings.name = "bicycle_embedded_super_coarse".to_string(); + settings.use_embedded_model = true; + settings.use_fine_mesh = true; + settings.background_resolution = None; + + build_bicycle_scene_from_settings(&settings) +} + +pub fn build_bicycle_scene_embedded_coarse(_params: &SceneParameters) -> Result> { + let mut settings = BicycleSceneSettings::default(); + settings.name = "bicycle_embedded_coarse".to_string(); + settings.use_embedded_model = true; + settings.use_fine_mesh = true; + settings.background_resolution = Some(1.0); + + build_bicycle_scene_from_settings(&settings) +} + +pub fn build_bicycle_scene_embedded_fine(_params: &SceneParameters) -> Result> { + let mut settings = BicycleSceneSettings::default(); + settings.name = "bicycle_embedded_fine".to_string(); + settings.use_embedded_model = true; + settings.use_fine_mesh = true; + settings.background_resolution = Some(0.3); + + build_bicycle_scene_from_settings(&settings) +} + +pub fn build_bicycle_scene_fem_coarse(_params: &SceneParameters) -> Result> { + let mut settings = BicycleSceneSettings::default(); + settings.name = "bicycle_fem_coarse".to_string(); + settings.use_embedded_model = false; + settings.use_fine_mesh = false; + + build_bicycle_scene_from_settings(&settings) +} + +pub fn build_bicycle_scene_fem_fine(_params: &SceneParameters) -> Result> { + let mut settings = BicycleSceneSettings::default(); + settings.name = "bicycle_fem_fine".to_string(); + settings.use_embedded_model = false; + settings.use_fine_mesh = true; + + build_bicycle_scene_from_settings(&settings) +} + +fn get_reference_cog() -> Result, Box> { + Ok(compute_mesh_cog(&*meshes::BIKE_TRI2D_MESH_FINE)) +} + +fn build_bicycle_scene_from_settings(settings: &BicycleSceneSettings) -> Result> { + let mut scene = Scene { + initial_state: Default::default(), + simulation_systems: Default::default(), + analysis_systems: Default::default(), + duration: settings.duration, + name: settings.name.clone(), + }; + + let embedded_mesh = if settings.use_fine_mesh { + meshes::BIKE_TRI2D_MESH_FINE.clone() + } else { + meshes::BIKE_TRI2D_MESH_COARSE.clone() + }; + let background_mesh = if let Some(bg_resolution) = settings.background_resolution { + generate_background_mesh_for_tri2d(&embedded_mesh, bg_resolution)? + } else { + generate_single_cell_background_mesh_for_tri2d(&embedded_mesh)? + }; + + let volume_mesh = embedded_mesh.clone(); + let surface_mesh = embedded_mesh.extract_surface_mesh(); + + let interior_quadrature = quad_quadrature_strength_5_f64(); + let triangle_quadrature = tri_quadrature_strength_5_f64(); + + let elastic_model = ElasticMaterialModel::from(StVKMaterial::from(YoungPoisson { + young: settings.young, + poisson: settings.poisson, + })); + + let own_cog = compute_mesh_cog(&embedded_mesh); + let ref_cog = get_reference_cog()?; + + let points_to_interpolate = vec![own_cog.into()]; + + if settings.use_embedded_model { + let model = construct_embedded_model_2d( + &background_mesh, + &embedded_mesh, + &triangle_quadrature, + interior_quadrature, + )?; + let fe_mesh = &background_mesh; + + let cog_component = PointInterpolator { + reference_points: points_to_interpolate.clone(), + interpolator: model.make_interpolator(&points_to_interpolate)?, + }; + + let entity = { + let body = BodyInitializer2d::initialize_in_state(&scene.initial_state); + body.add_finite_element_model(model, volume_mesh)? + .set_velocity(&apply_angular_velocity(settings.omega, &ref_cog, &fe_mesh))? + .add_material_surface(surface_mesh)? + .set_material(Material { + density: settings.density, + mass_damping_coefficient: None, + stiffness_damping_coefficient: None, + elastic_model, + }) + .add_name("embedded_bike"); + body.entity() + }; + + scene.initial_state.insert_component(entity, cog_component); + } else { + let model = NodalModel2d::from_mesh_and_quadrature(volume_mesh.clone(), triangle_quadrature.clone()); + let fe_mesh = &embedded_mesh; + + let cog_component = PointInterpolator { + reference_points: points_to_interpolate.clone(), + interpolator: model.make_interpolator(&points_to_interpolate)?, + }; + + let entity = { + let body = BodyInitializer2d::initialize_in_state(&scene.initial_state); + body.add_finite_element_model(model, volume_mesh)? + .set_velocity(&apply_angular_velocity(settings.omega, &ref_cog, &fe_mesh))? + .add_material_surface(surface_mesh)? + .set_material(Material { + density: settings.density, + mass_damping_coefficient: None, + stiffness_damping_coefficient: None, + elastic_model, + }) + .add_name("fem_bike"); + body.entity() + }; + + scene.initial_state.insert_component(entity, cog_component); + }; + + set_gravity(&mut scene.initial_state, 0.0); + + scene + .simulation_systems + .add_system(Box::new(FiniteElementIntegrator::default())); + scene + .simulation_systems + .add_system(Box::new(FiniteElementMeshDeformer)); + + Ok(scene) +} + +/// Computes the cog of a 2d triangle mesh +#[replace_float_literals(T::from_f64(literal).expect("Literal must fit in T"))] +fn compute_mesh_cog(mesh: &TriangleMesh2d) -> Vector2 +where + T: RealField, +{ + let compute_triangle_area = |v1: &Point2, v2: &Point2, v3: &Point2| -> T { + let x = v1.coords - v3.coords; + let y = v2.coords - v3.coords; + 0.5 * ((x.x * y.y) - (x.y * y.x)).abs() + }; + + let mut mesh_volume = T::zero(); + let mut mesh_cog = Vector2::zeros(); + for element in mesh.connectivity() { + let v0 = &mesh.vertices()[element[0]]; + let v1 = &mesh.vertices()[element[1]]; + let v2 = &mesh.vertices()[element[2]]; + + let element_cog = (v0.coords + v1.coords + v2.coords) * (3.0.recip()); + let element_volume = compute_triangle_area(v0, v1, v2); + mesh_volume += element_volume; + mesh_cog += element_cog * element_volume; + } + + mesh_cog * mesh_volume.recip() +} + +/// Computes a velocity field for the given mesh that represents a rotation +/// with the specified angular velocity around a center of rotation +fn apply_angular_velocity(omega: T, center: &Vector2, mesh: &Mesh2d) -> DVector +where + T: RealField, + C: Connectivity, +{ + let mut velocities = DVector::zeros(2 * mesh.vertices().len()); + for (i, v) in mesh.vertices().iter().enumerate() { + let r = v.coords - center; + let v_dir = r.yx(); + let v = v_dir * omega; + + velocities[2 * i + 0] = v.x; + velocities[2 * i + 1] = v.y; + } + + velocities +} diff --git a/simulation_toolbox/Cargo.toml b/simulation_toolbox/Cargo.toml new file mode 100644 index 0000000..f768631 --- /dev/null +++ b/simulation_toolbox/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "simulation_toolbox" +version = "0.1.0" +authors = ["Andreas Longva"] +edition = "2018" +publish = false + +[dependencies] +fenris = { path = "../fenris" } +hamilton = { path = "../hamilton" } +hamilton2 = { path = "../hamilton2" } +serde = "1.0" +itertools = "0.9" +ply-rs = "0.1.2" +obj = "0.10.0" +osqp = "0.6.0" +mshio = "0.4.2" +mkl-corrode = { git = "https://github.com/Andlon/mkl-corrode.git", rev="0843a0b46234cd88d7a0e7489720514624207ad9", features = [ "openmp" ] } +rstar = { version = "0.9.1", features = [ "serde" ] } +coarse-prof = "0.2" +log = "0.4" +num = "0.2" +serde_json = "1.0" +typetag = "0.1" +numeric_literals = "0.2" +rayon = "1.3" +paradis = { path = "../paradis" } +global_stash = { path = "../global_stash" } +nalgebra-lapack= { version="0.13", default-features=false, features = ["intel-mkl"] } +lapack-src = {version="0.5", features = ["intel-mkl"]} +# Make sure that +intel-mkl-src = { version="0.5", features = ["use-shared"]} +# This is a (temporary?) hack to force the openblas implementation used by nalgebra to use the system library +# rather than the bundled one +#openblas-src={version="0.8", features=["system"]} diff --git a/simulation_toolbox/src/components/mesh.rs b/simulation_toolbox/src/components/mesh.rs new file mode 100644 index 0000000..f7aa333 --- /dev/null +++ b/simulation_toolbox/src/components/mesh.rs @@ -0,0 +1,223 @@ +use std::error::Error; +use std::ops::{Deref, DerefMut}; +use std::path::PathBuf; + +use fenris::connectivity::Segment2d2Connectivity; +use fenris::geometry::polymesh::{PolyMesh2d, PolyMesh3d}; +use fenris::mesh::{Mesh2d, QuadMesh2d, TriangleMesh2d}; +use fenris::model::{FiniteElementInterpolator, MakeInterpolator}; +use fenris::nalgebra::{Point2, U2, U3}; +use hamilton::storages::VecStorage; +use hamilton::Component; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PolyMesh2dComponent { + /// Mesh name used for the output file as: {entity_name}_{mesh_name}_polymesh_{sequence_name}.{file_extension} + pub mesh_name: String, + /// Optional subfolder (relative to output directory) for the output of the meshes + pub subfolder: Option, + /// The polymesh to write to a file + pub mesh: PolyMesh2d, + /// Optional interpolator to interpolate the polymesh with on every output write + pub interpolator: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PolyMesh3dComponent { + /// Mesh name used for the output file as: {entity_name}_{mesh_name}_polymesh_{sequence_name}.{file_extension} + pub mesh_name: String, + /// Optional subfolder (relative to output directory) for the output of the meshes + pub subfolder: Option, + /// The polymesh to write to a file + pub mesh: PolyMesh3d, + /// Optional interpolator to interpolate the polymesh with on every output write + pub interpolator: Option>, +} + +impl PolyMesh3dComponent { + /// Creates a PolyMesh3dComponent for static geometry + pub fn new>(mesh_name: S, mesh: PolyMesh3d) -> Self { + PolyMesh3dComponent { + mesh_name: mesh_name.into(), + subfolder: None, + mesh, + interpolator: None, + } + } + + /// Attaches an interpolator to this PolyMesh3dComponent + pub fn with_interpolator>(mut self, model: &M) -> Result> { + self.interpolator = Some(model.make_interpolator(self.mesh.vertices())?); + Ok(self) + } + + /// Attaches a subfolder to this PolyMesh3dComponent + pub fn with_subfolder>(mut self, subfolder: P) -> Self { + self.subfolder = Some(subfolder.into()); + self + } +} + +impl PolyMesh2dComponent { + /// Creates a PolyMesh2dComponent for static geometry + pub fn new>(mesh_name: S, mesh: PolyMesh2d) -> Self { + PolyMesh2dComponent { + mesh_name: mesh_name.into(), + subfolder: None, + mesh, + interpolator: None, + } + } + + /// Attaches an interpolator to this PolyMesh2dComponent + pub fn with_interpolator>(mut self, model: &M) -> Result> { + self.interpolator = Some(model.make_interpolator(self.mesh.vertices())?); + Ok(self) + } + + /// Attaches a subfolder to this PolyMesh3dComponent + pub fn with_subfolder>(mut self, subfolder: P) -> Self { + self.subfolder = Some(subfolder.into()); + self + } +} + +/// Component storing interpolators for arbitrary 3D polymeshes +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PolyMesh2dCollection(pub Vec); + +/// Component storing interpolators for arbitrary 3D polymeshes +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PolyMesh3dCollection(pub Vec); + +impl Deref for PolyMesh2dCollection { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Component for PolyMesh2dCollection { + type Storage = VecStorage; +} + +impl Deref for PolyMesh3dCollection { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Component for PolyMesh3dCollection { + type Storage = VecStorage; +} + +/// Component storing an interpolator for a set of points +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PointInterpolator { + pub reference_points: Vec>, + pub interpolator: FiniteElementInterpolator, +} + +impl Component for PointInterpolator { + type Storage = VecStorage; +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +// TODO: Replace this with polygonal/polyhedral meshes later on +pub enum VolumeMesh2d { + QuadMesh(QuadMesh2d), + TriMesh(TriangleMesh2d), +} + +impl From> for VolumeMesh2d { + fn from(mesh: QuadMesh2d) -> Self { + Self::QuadMesh(mesh) + } +} + +impl From> for VolumeMesh2d { + fn from(mesh: TriangleMesh2d) -> Self { + Self::TriMesh(mesh) + } +} + +impl VolumeMesh2d { + pub fn vertices(&self) -> &[Point2] { + match self { + Self::QuadMesh(ref mesh) => mesh.vertices(), + Self::TriMesh(ref mesh) => mesh.vertices(), + } + } + + pub fn vertices_mut(&mut self) -> &mut [Point2] { + match self { + Self::QuadMesh(ref mut mesh) => mesh.vertices_mut(), + Self::TriMesh(ref mut mesh) => mesh.vertices_mut(), + } + } +} + +impl Component for VolumeMesh2d { + type Storage = VecStorage; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SurfaceMesh2d(pub Mesh2d); + +impl From> for SurfaceMesh2d { + fn from(mesh: Mesh2d) -> Self { + Self(mesh) + } +} + +impl Component for SurfaceMesh2d { + type Storage = VecStorage; +} + +impl Deref for SurfaceMesh2d { + type Target = Mesh2d; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SurfaceMesh2d { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct VolumeMesh3d(pub PolyMesh3d); + +impl Component for VolumeMesh3d { + type Storage = VecStorage; +} + +impl Deref for VolumeMesh3d { + type Target = PolyMesh3d; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for VolumeMesh3d { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl

From

for VolumeMesh3d +where + P: Into>, +{ + fn from(into_poly: P) -> Self { + Self(into_poly.into()) + } +} diff --git a/simulation_toolbox/src/components/mod.rs b/simulation_toolbox/src/components/mod.rs new file mode 100644 index 0000000..1bce36e --- /dev/null +++ b/simulation_toolbox/src/components/mod.rs @@ -0,0 +1,265 @@ +use std::convert::TryInto; +use std::error::Error; +use std::fmt; +use std::fmt::{Debug, Display}; + +use coarse_prof::profile; +use fenris::nalgebra::{Vector2, Vector3}; +use hamilton::storages::{ImmutableSingletonStorage, SingletonStorage, VecStorage}; +use hamilton::{Component, FilterSystem, RunOnceSystem, StorageContainer, System}; +use serde::{Deserialize, Serialize}; + +mod mesh; +pub use mesh::*; + +pub fn new_delayed_once_system(closure: F, wakeup_time: f64) -> impl System +where + F: FnOnce(&StorageContainer) -> Result<(), Box>, +{ + new_delayed_system(RunOnceSystem::new(closure), wakeup_time) +} + +pub fn new_delayed_system(system: S, wakeup_time: f64) -> impl System +where + S: System, +{ + let delay_predicate = { + let wakeup_time = wakeup_time; + let mut has_woken_up = false; + move |data: &StorageContainer| -> Result> { + if !has_woken_up { + has_woken_up = get_simulation_time(data)? >= wakeup_time; + Ok(has_woken_up) + } else { + Ok(false) + } + } + }; + + FilterSystem { + predicate: delay_predicate, + system, + } +} + +/// A system for timing the execution of a stored system +#[derive(Debug)] +pub struct TimingSystem +where + S: System, +{ + pub name: &'static str, + pub system: S, +} + +impl TimingSystem +where + S: System, +{ + pub fn new(name: &'static str, system: S) -> Self { + TimingSystem { name, system } + } +} + +impl Display for TimingSystem +where + S: System, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "TimingSystem for {}", self.name) + } +} + +impl System for TimingSystem +where + S: System, +{ + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + { + profile!(self.name); + self.system.run(data) + } + } +} + +/// A component that represents a name for the given entity. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Name(pub String); + +impl Display for Name { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Component for Name { + type Storage = VecStorage; +} + +impl From for Name +where + T: Into, +{ + fn from(string_like: T) -> Self { + Self(string_like.into()) + } +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub struct TimeStep(pub f64); + +impl Component for TimeStep { + // Note: for now we assume that time step is immutable. Different systems may of course + // still perform arbitrary time adaptivity within a single step if desirable. + type Storage = ImmutableSingletonStorage; +} + +impl From for f64 { + fn from(timestep: TimeStep) -> Self { + timestep.0 + } +} + +/// Models elapsed simulation time. +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub struct SimulationTime(pub f64); + +impl Component for SimulationTime { + type Storage = ImmutableSingletonStorage; +} + +impl SimulationTime { + pub fn elapsed(&self) -> f64 { + self.0 + } + + pub fn add(&self, delta: f64) -> Self { + Self(self.0 + delta) + } +} + +impl From for SimulationTime { + fn from(time: f64) -> Self { + Self(time) + } +} + +/// Models gravity. +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub enum Gravity { + Scalar(f64), + Vec2(Vector2), + Vec3(Vector3), +} + +impl Component for Gravity { + type Storage = SingletonStorage; +} + +impl From for Gravity { + fn from(gravity: f64) -> Self { + Gravity::Scalar(gravity) + } +} + +impl From> for Gravity { + fn from(gravity: Vector2) -> Self { + Gravity::Vec2(gravity) + } +} + +impl From> for Gravity { + fn from(gravity: Vector3) -> Self { + Gravity::Vec3(gravity) + } +} + +impl TryInto> for Gravity { + type Error = (); + + fn try_into(self) -> Result, ()> { + match self { + Gravity::Vec2(g) => Ok(g), + _ => Err(()), + } + } +} + +impl TryInto> for Gravity { + type Error = (); + + fn try_into(self) -> Result, ()> { + match self { + Gravity::Vec3(g) => Ok(g), + _ => Err(()), + } + } +} + +/// The index of the current step. +/// +/// The index of the initial state of a simulation will typically have index 0. +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub struct StepIndex(pub isize); + +impl Component for StepIndex { + type Storage = ImmutableSingletonStorage; +} + +/// The index in the sequence of exported data. +/// +/// The index of the initial state of a simulation will typically have index 0. +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub struct ExportSequenceIndex { + pub index: isize, + pub prev_export_time: Option, + pub export_interval: f64, +} + +impl Component for ExportSequenceIndex { + type Storage = ImmutableSingletonStorage; +} + +pub fn get_simulation_time(state: &StorageContainer) -> Result> { + state + .try_get_component_storage::() + .map(|refcell| refcell.borrow()) + .map(|storage| storage.get_component().elapsed()) + .ok_or_else(|| Box::::from("Failed to get SimulationTime component.")) +} + +pub fn get_time_step(state: &StorageContainer) -> Result> { + state + .try_get_component_storage::() + .map(|refcell| refcell.borrow()) + .map(|storage| f64::from(*storage.get_component())) + .ok_or_else(|| Box::::from("Failed to get TimeStep component.")) +} + +pub fn get_gravity(state: &StorageContainer) -> Result> { + state + .try_get_component_storage::() + .map(|refcell| refcell.borrow()) + .map(|storage| *storage.get_component()) + .ok_or_else(|| Box::::from("Failed to get Gravity component.")) +} + +pub fn set_gravity(state: &mut StorageContainer, new_gravity: impl Into) { + state.replace_storage(::Storage::new(new_gravity.into())); +} + +pub fn get_export_sequence_index(state: &StorageContainer) -> Result> { + state + .try_get_component_storage::() + .map(|refcell| refcell.borrow()) + .map(|storage| storage.get_component().index) + .ok_or_else(|| Box::::from("Failed to get ExportSequenceIndex component.")) +} + +pub fn get_step_index(state: &StorageContainer) -> Result> { + state + .try_get_component_storage::() + .map(|refcell| refcell.borrow()) + .map(|storage| storage.get_component().0) + .ok_or_else(|| Box::::from("Failed to get StepIndex component.")) +} diff --git a/simulation_toolbox/src/fem/bcs.rs b/simulation_toolbox/src/fem/bcs.rs new file mode 100644 index 0000000..06aeed6 --- /dev/null +++ b/simulation_toolbox/src/fem/bcs.rs @@ -0,0 +1,544 @@ +use std::error::Error; +use std::fmt::Debug; + +use fenris::nalgebra::allocator::Allocator; +use fenris::nalgebra::{DVector, DVectorSliceMut, DefaultAllocator, DimName, Point, Rotation3, Unit, VectorN, U2, U3}; +use hamilton::storages::VecStorage; +use hamilton::Component; +use serde::{Deserialize, Serialize}; + +use crate::util::all_items_unique; + +/// Trait for evaluating Dirichlet boundary conditions +#[typetag::serde(tag = "type")] +pub trait DirichletBoundaryConditions: Debug { + /// Returns the number of solution components per node. + fn solution_dim(&self) -> usize; + /// Returns the indices of the nodes that are affected by the boundary condition. + fn nodes(&self) -> &[usize]; + /// Returns the total number of rows of a boundary condition vector. + fn nrows(&self) -> usize { + self.solution_dim() * self.nodes().len() + } + /// Evaluates the displacement boundary conditions at the specified time. + fn apply_displacement_bcs(&self, u: DVectorSliceMut, t: f64); + /// Evaluates the velocity boundary conditions at the specified time. + fn apply_velocity_bcs(&self, v: DVectorSliceMut, t: f64); +} + +/// Helper trait to implement an interface parallel to `DirichletBoundaryConditions` on `Option<&dyn DirichletBoundaryConditions>` +pub trait OptionalDirichletBoundaryConditions { + /// Returns the number of solution components per node. + fn solution_dim(&self) -> usize; + /// Returns the indices of the nodes that are affected by the boundary condition. + fn nodes(&self) -> &[usize]; + /// Returns the total number of rows of a boundary condition vector. + fn nrows(&self) -> usize; + /// Evaluates the displacement boundary conditions at the specified time. + fn apply_displacement_bcs(&self, u: DVectorSliceMut, t: f64); + /// Evaluates the velocity boundary conditions at the specified time. + fn apply_velocity_bcs(&self, v: DVectorSliceMut, t: f64); +} + +/// Component for storing Dirichlet boundary conditions +#[derive(Debug, Serialize, Deserialize)] +pub struct DirichletBoundaryConditionComponent { + pub bc: Box, +} + +/// Dummy boundary condition that does not prescribe anything +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Empty; + +/// Homogeneous Dirichlet boundary condition that prescribes zero values (i.e. overwrites them to zero) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Homogeneous { + pub dim: usize, + pub static_nodes: Vec, +} + +/// Dirichlet boundary condition that adds a fixed displacement +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConstantDisplacement { + dim: usize, + nodes: Vec, + displacement: DVector, +} + +/// Boundary condition that adds a constant and uniform displacement to the given nodes +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(bound( + serialize = "VectorN: Serialize", + deserialize = "VectorN: Deserialize<'de>" +))] +pub struct ConstantUniformDisplacement +where + D: DimName, + DefaultAllocator: Allocator, +{ + nodes: Vec, + displacement: VectorN, +} + +/// Boundary condition that adds a constant and uniform linear velocity to the given nodes +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(bound( + serialize = "VectorN: Serialize", + deserialize = "VectorN: Deserialize<'de>" +))] +pub struct ConstantUniformVelocity +where + D: DimName, + DefaultAllocator: Allocator, +{ + nodes: Vec, + velocity: VectorN, +} + +/// Boundary condition that applies a constant and uniform angular velocity to the given nodes +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(bound( + serialize = "VectorN: Serialize, Point: Serialize", + deserialize = "VectorN: Deserialize<'de>, Point: Deserialize<'de>" +))] +pub struct ConstantUniformAngularVelocity +where + D: DimName, + DefaultAllocator: Allocator, +{ + /// Node indices affected by this boundary condition + nodes: Vec, + /// Angular velocity of the nodes + omega: VectorN, + /// Center of rotation + center: Point, + /// Initial positions of boundary nodes + x0: Vec>, +} + +/// Union of multiple disjoint Dirichlet boundary conditions +#[derive(Debug, Serialize, Deserialize)] +pub struct Union { + dim: usize, + nodes: Vec, + bcs: Vec>, +} + +impl From for Box { + fn from(bc: T) -> Box { + Box::new(bc) + } +} + +impl From for DirichletBoundaryConditionComponent { + fn from(bcs: T) -> Self { + Self { bc: bcs.into() } + } +} + +impl From> for DirichletBoundaryConditionComponent { + fn from(bcs: Box) -> Self { + Self { bc: bcs } + } +} + +impl Component for DirichletBoundaryConditionComponent { + type Storage = VecStorage; +} + +#[typetag::serde] +impl DirichletBoundaryConditions for Empty { + fn solution_dim(&self) -> usize { + 0 + } + fn nodes(&self) -> &[usize] { + &[] + } + fn apply_displacement_bcs(&self, _u: DVectorSliceMut, _t: f64) {} + fn apply_velocity_bcs(&self, _v: DVectorSliceMut, _t: f64) {} +} + +impl Homogeneous { + pub fn new(dim: usize, static_nodes: &[usize]) -> Self { + Self { + dim, + static_nodes: static_nodes.to_vec(), + } + } + + /// Creates 2D homogeneous boundary conditions for the specified node indices + pub fn new_2d(static_nodes: &[usize]) -> Self { + Self::new(2, static_nodes) + } + + /// Creates 3D homogeneous boundary conditions for the specified node indices + pub fn new_3d(static_nodes: &[usize]) -> Self { + Self::new(3, static_nodes) + } +} + +#[typetag::serde] +impl DirichletBoundaryConditions for Homogeneous { + fn solution_dim(&self) -> usize { + self.dim + } + + fn nodes(&self) -> &[usize] { + &self.static_nodes + } + + fn apply_displacement_bcs(&self, mut u: DVectorSliceMut, _t: f64) { + assert!(u.nrows() == self.nrows()); + u.fill(0.0); + } + + fn apply_velocity_bcs(&self, mut v: DVectorSliceMut, _t: f64) { + assert!(v.nrows() == self.nrows()); + v.fill(0.0); + } +} + +impl ConstantDisplacement { + pub fn new(dim: usize, nodes: &[usize], displacement: DVector) -> Self { + assert!(displacement.nrows() == dim * nodes.len()); + Self { + dim, + nodes: nodes.to_vec(), + displacement, + } + } + + pub fn new_2d(nodes: &[usize], displacement: DVector) -> Self { + Self::new(2, nodes, displacement) + } + + pub fn new_3d(nodes: &[usize], displacement: DVector) -> Self { + Self::new(3, nodes, displacement) + } +} + +#[typetag::serde] +impl DirichletBoundaryConditions for ConstantDisplacement { + fn solution_dim(&self) -> usize { + self.dim + } + + fn nodes(&self) -> &[usize] { + &self.nodes + } + + fn apply_displacement_bcs(&self, mut u: DVectorSliceMut, _t: f64) { + assert!(u.nrows() == self.nrows()); + u += &self.displacement; + } + + fn apply_velocity_bcs(&self, _v: DVectorSliceMut, _t: f64) {} +} + +impl Union { + /// Returns the union of the supplied Dirichlet boundary conditions if they are disjoint + /// + /// Note that the node indices affected by the BCs are cached on construction. + /// Therefore the stored BCs should not modify their set of boundary nodes. + pub fn try_new(bcs: Vec>) -> Result> { + // Obtain the solution dimension of all supplied BCs + let mut bc_iter = bcs.iter(); + let dim = if let Some(first_bc) = bc_iter.next() { + let first_dim = first_bc.solution_dim(); + if bc_iter.any(|bc| bc.solution_dim() != first_dim) { + return Err(Box::::from("BCs do not have the same dimension")); + } + first_dim + } else { + return Ok(Self { + dim: 0, + nodes: Vec::new(), + bcs: Vec::new(), + }); + }; + + // Collect the constrained nodes of all bcs + let all_nodes = bcs + .iter() + .map(|bc| bc.nodes()) + .fold(Vec::new(), |mut vec, elems| { + vec.extend(elems); + vec + }); + + // Check if the boundary conditions are disjoint + if all_items_unique(&all_nodes) { + Ok(Self { + dim, + nodes: all_nodes, + bcs, + }) + } else { + Err(Box::::from("BCs are not disjoint")) + } + } +} + +#[typetag::serde] +impl DirichletBoundaryConditions for Union { + fn solution_dim(&self) -> usize { + self.dim + } + + fn nodes(&self) -> &[usize] { + &self.nodes + } + + fn apply_displacement_bcs(&self, mut u: DVectorSliceMut, t: f64) { + assert!(u.nrows() == self.nrows()); + + let mut offset = 0; + for bc in &self.bcs { + let len = bc.nrows(); + bc.apply_displacement_bcs(u.rows_mut(offset, len), t); + offset += len; + } + } + + fn apply_velocity_bcs(&self, mut v: DVectorSliceMut, t: f64) { + assert!(v.nrows() == self.nrows()); + + let mut offset = 0; + for bc in &self.bcs { + let len = bc.nrows(); + bc.apply_velocity_bcs(v.rows_mut(offset, len), t); + offset += len; + } + } +} + +impl ConstantUniformDisplacement +where + D: DimName, + DefaultAllocator: Allocator, + VectorN: Serialize, +{ + pub fn new(nodes: &[usize], displacement: VectorN) -> Self { + Self { + nodes: nodes.to_vec(), + displacement, + } + } + + fn solution_dim(&self) -> usize { + D::dim() + } + + fn nodes(&self) -> &[usize] { + &self.nodes + } + + fn apply_displacement_bcs(&self, mut u: DVectorSliceMut, _t: f64) { + assert!(u.nrows() == D::dim() * self.nodes.len()); + + for i in 0..self.nodes.len() { + let mut ui = u.fixed_rows_mut::(D::dim() * i); + ui.axpy(1.0, &self.displacement, 1.0); + } + } + + fn apply_velocity_bcs(&self, v: DVectorSliceMut, _t: f64) { + assert!(v.nrows() == D::dim() * self.nodes.len()); + } +} + +#[typetag::serde] +impl DirichletBoundaryConditions for ConstantUniformDisplacement { + fn solution_dim(&self) -> usize { + self.solution_dim() + } + + fn nodes(&self) -> &[usize] { + self.nodes() + } + + fn apply_displacement_bcs(&self, u: DVectorSliceMut, t: f64) { + self.apply_displacement_bcs(u, t) + } + + fn apply_velocity_bcs(&self, v: DVectorSliceMut, t: f64) { + self.apply_velocity_bcs(v, t) + } +} + +#[typetag::serde] +impl DirichletBoundaryConditions for ConstantUniformDisplacement { + fn solution_dim(&self) -> usize { + self.solution_dim() + } + + fn nodes(&self) -> &[usize] { + self.nodes() + } + + fn apply_displacement_bcs(&self, u: DVectorSliceMut, t: f64) { + self.apply_displacement_bcs(u, t) + } + + fn apply_velocity_bcs(&self, v: DVectorSliceMut, t: f64) { + self.apply_velocity_bcs(v, t) + } +} + +impl ConstantUniformVelocity +where + D: DimName, + DefaultAllocator: Allocator, + VectorN: Serialize, +{ + pub fn new(nodes: &[usize], velocity: VectorN) -> Self { + Self { + nodes: nodes.to_vec(), + velocity, + } + } + + fn solution_dim(&self) -> usize { + D::dim() + } + + fn nodes(&self) -> &[usize] { + &self.nodes + } + + fn apply_displacement_bcs(&self, mut u: DVectorSliceMut, t: f64) { + assert!(u.nrows() == D::dim() * self.nodes.len()); + + for i in 0..self.nodes.len() { + let mut ui = u.fixed_rows_mut::(D::dim() * i); + ui.axpy(t, &self.velocity, 1.0); + } + } + + fn apply_velocity_bcs(&self, mut v: DVectorSliceMut, _t: f64) { + assert!(v.nrows() == D::dim() * self.nodes.len()); + + for i in 0..self.nodes.len() { + let mut vi = v.fixed_rows_mut::(D::dim() * i); + vi.axpy(1.0, &self.velocity, 1.0); + } + } +} + +#[typetag::serde] +impl DirichletBoundaryConditions for ConstantUniformVelocity { + fn solution_dim(&self) -> usize { + self.solution_dim() + } + + fn nodes(&self) -> &[usize] { + self.nodes() + } + + fn apply_displacement_bcs(&self, u: DVectorSliceMut, t: f64) { + self.apply_displacement_bcs(u, t) + } + + fn apply_velocity_bcs(&self, v: DVectorSliceMut, t: f64) { + self.apply_velocity_bcs(v, t) + } +} + +#[typetag::serde] +impl DirichletBoundaryConditions for ConstantUniformVelocity { + fn solution_dim(&self) -> usize { + self.solution_dim() + } + + fn nodes(&self) -> &[usize] { + self.nodes() + } + + fn apply_displacement_bcs(&self, u: DVectorSliceMut, t: f64) { + self.apply_displacement_bcs(u, t) + } + + fn apply_velocity_bcs(&self, v: DVectorSliceMut, t: f64) { + self.apply_velocity_bcs(v, t) + } +} + +impl ConstantUniformAngularVelocity +where + D: DimName, + DefaultAllocator: Allocator, + VectorN: Serialize, + Point: Serialize, +{ + pub fn new(nodes: &[usize], omega: VectorN, center: Point, x0: Vec>) -> Self { + Self { + nodes: nodes.to_vec(), + omega, + center, + x0, + } + } +} + +#[typetag::serde] +impl DirichletBoundaryConditions for ConstantUniformAngularVelocity { + fn solution_dim(&self) -> usize { + 3 + } + + fn nodes(&self) -> &[usize] { + &self.nodes + } + + fn apply_displacement_bcs(&self, mut u: DVectorSliceMut, t: f64) { + let rot_angle = (self.omega.norm() * t) % (2.0 * std::f64::consts::PI); + let rot = Rotation3::from_axis_angle(&Unit::new_normalize(self.omega.clone()), rot_angle); + + for i in 0..self.nodes.len() { + let mut ui = u.fixed_rows_mut::(3 * i); + let r = &self.x0[i] - &self.center; + let r_rot = &rot * r; + let dr = r_rot - r; + ui.copy_from(&dr); + } + } + + fn apply_velocity_bcs(&self, mut v: DVectorSliceMut, t: f64) { + // TODO: This doesn't work properly with the apply method, because it only applies itself + let mut u = DVector::zeros(3 * self.nodes.len()); + self.apply_displacement_bcs(DVectorSliceMut::from(&mut u), t); + + for i in 0..self.nodes.len() { + let ui = u.fixed_rows::(3 * i); + let mut vi = v.fixed_rows_mut::(3 * i); + let v_tang = self.omega.cross(&ui); + vi.copy_from(&v_tang); + } + } +} + +impl OptionalDirichletBoundaryConditions for Option<&dyn DirichletBoundaryConditions> { + fn solution_dim(&self) -> usize { + self.map(|bc| bc.solution_dim()).unwrap_or(0) + } + + fn nodes(&self) -> &[usize] { + self.map(|bc| bc.nodes()).unwrap_or(&[]) + } + + fn nrows(&self) -> usize { + self.map(|bc| bc.nrows()).unwrap_or(0) + } + + fn apply_displacement_bcs(&self, u: DVectorSliceMut, t: f64) { + if let Some(bc) = self { + bc.apply_displacement_bcs(u, t); + } + } + + fn apply_velocity_bcs(&self, v: DVectorSliceMut, t: f64) { + if let Some(bc) = self { + bc.apply_velocity_bcs(v, t); + } + } +} diff --git a/simulation_toolbox/src/fem/deformer.rs b/simulation_toolbox/src/fem/deformer.rs new file mode 100644 index 0000000..15e4270 --- /dev/null +++ b/simulation_toolbox/src/fem/deformer.rs @@ -0,0 +1,110 @@ +use crate::components::{SurfaceMesh2d, VolumeMesh2d, VolumeMesh3d}; +use crate::fem::{FiniteElementElasticModel2d, FiniteElementElasticModel3d}; +use crate::util::apply_displacements; +use fenris::nalgebra::{U2, U3}; +use fenris::space::FiniteElementSpace; +use hamilton::{StorageContainer, System}; +use std::error::Error; +use std::fmt; +use std::fmt::Display; + +#[derive(Debug)] +pub struct FiniteElementMeshDeformer; + +impl Display for FiniteElementMeshDeformer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "FiniteElementMeshDeformer") + } +} + +impl System for FiniteElementMeshDeformer { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + // 2D + { + let fe_models = data + .get_component_storage::() + .borrow(); + let mut volume_meshes = data.get_component_storage::().borrow_mut(); + let mut surface_meshes = data.get_component_storage::().borrow_mut(); + + for (id, model) in fe_models.entity_component_iter() { + // Volume mesh + if let Some(mesh) = volume_meshes.get_component_mut(*id) { + let interpolator = &model.material_volume_interpolator; + let n_vertices = model.material_volume_mesh.vertices().len(); + + if n_vertices == mesh.vertices().len() { + // TODO: Reuse vector instead of re-allocating on every `run` invocation + let displacements = interpolator.interpolate::(&model.u); + apply_displacements( + mesh.vertices_mut(), + model.material_volume_mesh.vertices(), + &displacements, + ); + } else { + return Err(Box::from( + "Reference mesh and deformed mesh have incompatible vertex counts.", + )); + } + } + + // Surface mesh + if let Some(surface_mesh) = surface_meshes.get_component_mut(*id) { + let ref_surface = &model.material_surface; + let interpolator = &model.material_surface_interpolator; + + if let (Some(ref_surface), Some(interpolator)) = (ref_surface, interpolator) { + let n_vertices = FiniteElementSpace::vertices(ref_surface).len(); + if n_vertices == surface_mesh.vertices().len() { + // TODO: Reuse vector instead of re-allocating on every `run` invocation + let displacements = interpolator.interpolate::(&model.u); + surface_mesh.transform_all_vertices(|vertices_mut| { + apply_displacements( + vertices_mut, + FiniteElementSpace::vertices(ref_surface), + &displacements, + ); + }); + } else { + return Err(Box::from( + "Reference and deformed surface meshes have incompatible vertex counts", + )); + } + } + } + } + } + + // 3D + { + let fe_models = data + .get_component_storage::() + .borrow(); + let mut volume_meshes = data.get_component_storage::().borrow_mut(); + + for (id, model) in fe_models.entity_component_iter() { + // Volume mesh + if let Some(mesh) = volume_meshes.get_component_mut(*id) { + let interpolator = &model.material_volume_interpolator; + let n_vertices = model.material_volume_mesh.vertices().len(); + + if n_vertices == mesh.vertices().len() { + // TODO: Reuse vector instead of re-allocating on every `run` invocation + let displacements = interpolator.interpolate::(&model.u); + apply_displacements( + mesh.vertices_mut(), + model.material_volume_mesh.vertices(), + &displacements, + ); + } else { + return Err(Box::from( + "Reference mesh and deformed mesh have incompatible vertex counts.", + )); + } + } + } + } + + Ok(()) + } +} diff --git a/simulation_toolbox/src/fem/fe_model.rs b/simulation_toolbox/src/fem/fe_model.rs new file mode 100644 index 0000000..58537b7 --- /dev/null +++ b/simulation_toolbox/src/fem/fe_model.rs @@ -0,0 +1,701 @@ +use std::fmt::Debug; +use std::ops::Add; + +use coarse_prof::profile; +use fenris::assembly::{apply_homogeneous_dirichlet_bc_csr, ElementMatrixTransformation}; +use fenris::connectivity::{ + Hex20Connectivity, Hex27Connectivity, Hex8Connectivity, Segment2d2Connectivity, Tet10Connectivity, Tet4Connectivity, +}; +use fenris::embedding::{ + EmbeddedModel3d, EmbeddedQuad4Model, EmbeddedQuad9Model, EmbeddedTri3Model, EmbeddedTri6Model, +}; +use fenris::mesh::ClosedSurfaceMesh2d; +use fenris::model::{FiniteElementInterpolator, NodalModel3d, Quad4Model, Quad9Model, Tri3d2Model, Tri6d2Model}; +use fenris::nalgebra::{ + DVector, DVectorSlice, DVectorSliceMut, Matrix2, Matrix3, UnitQuaternion, Vector2, Vector3, U2, U3, +}; +use fenris::solid::assembly::ScalarMaterialSpaceFunction; +use fenris::solid::materials::{ + CorotatedLinearElasticMaterial, InvertibleMaterial, LinearElasticMaterial, ProjectedStableNeoHookeanMaterial, + StVKMaterial, StableNeoHookeanMaterial, YoungPoisson, +}; +use fenris::solid::{ElasticityModel, ElasticityModelParallel}; +use fenris::{solid, CooMatrix, CsrMatrix}; +use hamilton::storages::VecStorage; +use hamilton::Component; +use log::info; +use mkl_corrode::dss; +use serde::{Deserialize, Serialize}; + +use crate::components::{VolumeMesh2d, VolumeMesh3d}; +use crate::fem::bcs::{DirichletBoundaryConditions, OptionalDirichletBoundaryConditions}; +use crate::fem::IntegrationMethod; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ElasticMaterialModel { + LinearElastic(LinearElasticMaterial), + CorotatedLinearElastic(CorotatedLinearElasticMaterial), + StableNeoHookean(StableNeoHookeanMaterial), + ProjectedStableNeoHookean(ProjectedStableNeoHookeanMaterial), + StVenantKirchhoff(StVKMaterial), + InvertibleLinearElastic(InvertibleMaterial>), + InvertibleStableNeoHookean(InvertibleMaterial>), + InvertibleStVenantKirchhoff(InvertibleMaterial>), +} + +/// Helper macro to call e.g. a generic method on the specific underlying material type of +/// an instance of `ElasticMaterialModel`. +#[macro_export] +macro_rules! match_on_elastic_material_model { + ($on_model:expr, $model:ident => $e:expr) => {{ + use $crate::fem::ElasticMaterialModel::*; + match $on_model { + LinearElastic(ref $model) => $e, + CorotatedLinearElastic(ref $model) => $e, + StableNeoHookean(ref $model) => $e, + ProjectedStableNeoHookean(ref $model) => $e, + StVenantKirchhoff(ref $model) => $e, + InvertibleLinearElastic(ref $model) => $e, + InvertibleStableNeoHookean(ref $model) => $e, + InvertibleStVenantKirchhoff(ref $model) => $e, + } + }}; +} + +impl fenris::solid::ElasticMaterialModel for ElasticMaterialModel { + fn compute_strain_energy_density(&self, deformation_gradient: &Matrix2) -> f64 { + match_on_elastic_material_model!(self, + material => material.compute_strain_energy_density(deformation_gradient)) + } + + fn compute_stress_tensor(&self, deformation_gradient: &Matrix2) -> Matrix2 { + match_on_elastic_material_model!(self, + material => material.compute_stress_tensor(deformation_gradient)) + } + + fn contract_stress_tensor_with( + &self, + deformation_gradient: &Matrix2, + a: &Vector2, + b: &Vector2, + ) -> Matrix2 { + match_on_elastic_material_model!(self, + material => material.contract_stress_tensor_with(deformation_gradient, a, b)) + } +} + +impl fenris::solid::ElasticMaterialModel for ElasticMaterialModel { + fn compute_strain_energy_density(&self, deformation_gradient: &Matrix3) -> f64 { + match_on_elastic_material_model!(self, + material => material.compute_strain_energy_density(deformation_gradient)) + } + + fn compute_stress_tensor(&self, deformation_gradient: &Matrix3) -> Matrix3 { + match_on_elastic_material_model!(self, + material => material.compute_stress_tensor(deformation_gradient)) + } + + fn contract_stress_tensor_with( + &self, + deformation_gradient: &Matrix3, + a: &Vector3, + b: &Vector3, + ) -> Matrix3 { + match_on_elastic_material_model!(self, + material => material.contract_stress_tensor_with(deformation_gradient, a, b)) + } +} + +macro_rules! impl_from_material { + ($material:ty, $invariant:ident) => { + impl From<$material> for ElasticMaterialModel { + fn from(material: $material) -> Self { + use ElasticMaterialModel::*; + $invariant(material) + } + } + }; +} + +impl_from_material!(StableNeoHookeanMaterial, StableNeoHookean); +impl_from_material!(ProjectedStableNeoHookeanMaterial, ProjectedStableNeoHookean); +impl_from_material!(StVKMaterial, StVenantKirchhoff); +impl_from_material!(LinearElasticMaterial, LinearElastic); +impl_from_material!(CorotatedLinearElasticMaterial, CorotatedLinearElastic); +impl_from_material!(InvertibleMaterial>, InvertibleLinearElastic); +impl_from_material!(InvertibleMaterial>, InvertibleStVenantKirchhoff); +impl_from_material!(InvertibleMaterial>, + InvertibleStableNeoHookean); + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Material { + pub density: f64, + pub mass_damping_coefficient: Option, + pub stiffness_damping_coefficient: Option, + pub elastic_model: ElasticMaterialModel, +} + +pub fn default_young_poisson() -> YoungPoisson { + YoungPoisson { + young: 1e5, + poisson: 0.4, + } +} + +impl Default for Material { + fn default() -> Self { + let material = StableNeoHookeanMaterial::from(default_young_poisson()); + let inversion_threshold = 1e-6; + let material = InvertibleMaterial::new(material, inversion_threshold); + Self { + /// Default density is adapted to 3D density for water, e.g. 1000 kg/m3 + density: 1000.0, + mass_damping_coefficient: None, + stiffness_damping_coefficient: None, + elastic_model: ElasticMaterialModel::from(material), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum FiniteElementModel2d { + // TODO: Implement more models + Tri3d2NodalModel(Tri3d2Model), + Tri6d2NodalModel(Tri6d2Model), + Quad4NodalModel(Quad4Model), + Quad9NodalModel(Quad9Model), + EmbeddedTri3d2Model(EmbeddedTri3Model), + EmbeddedTri6d2Model(EmbeddedTri6Model), + EmbeddedQuad4Model(EmbeddedQuad4Model), + EmbeddedQuad9Model(EmbeddedQuad9Model), +} + +impl From> for FiniteElementModel2d { + fn from(model: Tri3d2Model) -> Self { + Self::Tri3d2NodalModel(model) + } +} + +impl From> for FiniteElementModel2d { + fn from(model: Tri6d2Model) -> Self { + Self::Tri6d2NodalModel(model) + } +} + +impl From> for FiniteElementModel2d { + fn from(model: Quad4Model) -> Self { + Self::Quad4NodalModel(model) + } +} + +impl From> for FiniteElementModel2d { + fn from(model: Quad9Model) -> Self { + Self::Quad9NodalModel(model) + } +} + +impl From> for FiniteElementModel2d { + fn from(model: EmbeddedTri3Model) -> Self { + Self::EmbeddedTri3d2Model(model) + } +} + +impl From> for FiniteElementModel2d { + fn from(model: EmbeddedTri6Model) -> Self { + Self::EmbeddedTri6d2Model(model) + } +} + +impl From> for FiniteElementModel2d { + fn from(model: EmbeddedQuad4Model) -> Self { + Self::EmbeddedQuad4Model(model) + } +} + +impl From> for FiniteElementModel2d { + fn from(model: EmbeddedQuad9Model) -> Self { + Self::EmbeddedQuad9Model(model) + } +} + +/// Helper macro to call e.g. a generic method on the specific underlying model type of +/// an instance of `FiniteElementModel`. +#[macro_export] +macro_rules! match_on_finite_element_model_2d { + ($on_model:expr, $model:ident => $e:expr) => {{ + use $crate::fem::FiniteElementModel2d::*; + match $on_model { + Tri3d2NodalModel(ref $model) => $e, + Tri6d2NodalModel(ref $model) => $e, + Quad4NodalModel(ref $model) => $e, + Quad9NodalModel(ref $model) => $e, + EmbeddedTri3d2Model(ref $model) => $e, + EmbeddedTri6d2Model(ref $model) => $e, + EmbeddedQuad4Model(ref $model) => $e, + EmbeddedQuad9Model(ref $model) => $e, + } + }}; +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum FiniteElementModel3d { + // TODO: Implement more models + Hex8NodalModel(NodalModel3d), + Hex20NodalModel(NodalModel3d), + Hex27NodalModel(NodalModel3d), + Tet4NodalModel(NodalModel3d), + Tet10NodalModel(NodalModel3d), + EmbeddedHex8Model(EmbeddedModel3d), + EmbeddedHex20Model(EmbeddedModel3d), + EmbeddedHex27Model(EmbeddedModel3d), + EmbeddedTet4Model(EmbeddedModel3d), + EmbeddedTet10Model(EmbeddedModel3d), +} + +#[macro_export] +macro_rules! match_on_nodal_element_model_3d { + ($on_model:expr, $model:ident => $e:expr, $fallback:expr) => {{ + use $crate::fem::FiniteElementModel3d::*; + match $on_model { + Hex8NodalModel(ref $model) => $e, + Hex20NodalModel(ref $model) => $e, + Hex27NodalModel(ref $model) => $e, + Tet4NodalModel(ref $model) => $e, + Tet10NodalModel(ref $model) => $e, + EmbeddedHex8Model(_) => $fallback, + EmbeddedHex20Model(_) => $fallback, + EmbeddedHex27Model(_) => $fallback, + EmbeddedTet4Model(_) => $fallback, + EmbeddedTet10Model(_) => $fallback, + } + }}; +} + +#[macro_export] +macro_rules! match_on_finite_element_model_3d { + ($on_model:expr, $model:ident => $e:expr) => {{ + use $crate::fem::FiniteElementModel3d::*; + match $on_model { + Hex8NodalModel(ref $model) => $e, + Hex20NodalModel(ref $model) => $e, + Hex27NodalModel(ref $model) => $e, + Tet4NodalModel(ref $model) => $e, + Tet10NodalModel(ref $model) => $e, + EmbeddedHex8Model(ref $model) => $e, + EmbeddedHex20Model(ref $model) => $e, + EmbeddedHex27Model(ref $model) => $e, + EmbeddedTet4Model(ref $model) => $e, + EmbeddedTet10Model(ref $model) => $e, + } + }}; +} + +impl From> for FiniteElementModel3d { + fn from(model: NodalModel3d) -> Self { + Self::Hex8NodalModel(model) + } +} + +impl From> for FiniteElementModel3d { + fn from(model: NodalModel3d) -> Self { + Self::Hex20NodalModel(model) + } +} + +impl From> for FiniteElementModel3d { + fn from(model: NodalModel3d) -> Self { + Self::Hex27NodalModel(model) + } +} + +impl From> for FiniteElementModel3d { + fn from(model: NodalModel3d) -> Self { + Self::Tet4NodalModel(model) + } +} + +impl From> for FiniteElementModel3d { + fn from(model: NodalModel3d) -> Self { + Self::Tet10NodalModel(model) + } +} + +impl From> for FiniteElementModel3d { + fn from(model: EmbeddedModel3d) -> Self { + Self::EmbeddedHex8Model(model) + } +} + +impl From> for FiniteElementModel3d { + fn from(model: EmbeddedModel3d) -> Self { + Self::EmbeddedHex20Model(model) + } +} + +impl From> for FiniteElementModel3d { + fn from(model: EmbeddedModel3d) -> Self { + Self::EmbeddedHex27Model(model) + } +} + +impl From> for FiniteElementModel3d { + fn from(model: EmbeddedModel3d) -> Self { + Self::EmbeddedTet4Model(model) + } +} + +impl From> for FiniteElementModel3d { + fn from(model: EmbeddedModel3d) -> Self { + Self::EmbeddedTet10Model(model) + } +} + +#[derive(Debug)] +pub struct ElasticModelMatrixStorage { + pub mass_matrix: CsrMatrix, + // TODO: Better solution than a separate bool. Something like an Option + // that keeps it's value when going from Some -> None -> Some. + pub has_damping_matrix: bool, + pub damping_matrix: CsrMatrix, + pub stiffness_matrix: CsrMatrix, + pub linear_combination_apply: CsrMatrix, + pub linear_combination_solve: CsrMatrix, + + /// Used for stopping criteria of integrators, + /// e.g. ||M v_dot - f|| <= eps * representative_force + /// TODO: Move this somewhere else + pub representative_force: f64, +} + +impl ElasticModelMatrixStorage { + fn new_from_mass(mass: CsrMatrix, representative_force: f64) -> Self { + let mut damping_matrix = mass.clone(); + damping_matrix.fill_par(0.0); + + let stiffness_matrix = damping_matrix.clone(); + let linear_combination_apply = damping_matrix.clone(); + let linear_combination_solve = damping_matrix.clone(); + + ElasticModelMatrixStorage { + mass_matrix: mass, + has_damping_matrix: false, + damping_matrix, + stiffness_matrix, + linear_combination_apply, + linear_combination_solve, + representative_force, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FiniteElementElasticModel2d { + pub model: FiniteElementModel2d, + pub u: DVector, + pub v: DVector, + pub material_volume_mesh: VolumeMesh2d, + pub material_volume_interpolator: FiniteElementInterpolator, + + #[serde(skip)] + // Cache the factorization here so that we can reuse the symbolic factorization + // Any system making changes to topology should set this variable to `None` + // to force a refactorization + pub factorization: Option>, + + // Material surface is used for deforming contact geometry, + // so the surface mesh must be compatible with contact geometry + pub material_surface: Option>, + pub material_surface_interpolator: Option>, + pub material: Material, + + pub integrator: IntegrationMethod, + + // TODO: All these should be stored in a separate components + pub gravity_enabled: bool, + #[serde(skip)] + pub model_matrix_storage: Option, +} + +impl Component for FiniteElementElasticModel2d { + type Storage = VecStorage; +} + +fn compute_representative_force(mass_matrix: &CsrMatrix, dim: usize) -> f64 { + // TODO: Don't hardcode this + let representative_acceleration_norm = 10.0; + let a = representative_acceleration_norm; + // This is just an algebraic trick to ensure that the acceleration vector + // is consistent independent of direction, but still has the correct acceleration norm + let representative_acceleration = DVector::repeat(mass_matrix.nrows(), ((a * a) / dim as f64).sqrt()); + (mass_matrix * &representative_acceleration).norm() +} + +impl FiniteElementElasticModel2d { + pub fn ensure_model_matrix_storage_initialized(&mut self, dirichlet_bcs: Option<&dyn DirichletBoundaryConditions>) { + if self.model_matrix_storage.is_none() { + let mut mass_matrix = self + .model + .assemble_mass(self.material.density) + .to_csr(Add::add); + apply_homogeneous_dirichlet_bc_csr::(&mut mass_matrix, dirichlet_bcs.nodes()); + info!( + "Assembled mass matrix ({} x {}): {} nnz", + mass_matrix.nrows(), + mass_matrix.ncols(), + mass_matrix.nnz() + ); + + let representative_force = compute_representative_force(&mass_matrix, 2); + self.model_matrix_storage = Some(ElasticModelMatrixStorage::new_from_mass( + mass_matrix, + representative_force, + )); + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FiniteElementElasticModel3d { + pub model: FiniteElementModel3d, + pub u: DVector, + pub v: DVector, + + #[serde(skip)] + // Cache the factorization here so that we can reuse the symbolic factorization + // Any system making changes to topology should set this variable to `None` + // to force a refactorization + pub factorization: Option>, + + pub material_volume_mesh: VolumeMesh3d, + pub material_volume_interpolator: FiniteElementInterpolator, + + pub material: Material, + + pub integrator: IntegrationMethod, + + // TODO: All these should be stored in a separate components + pub gravity_enabled: bool, + + #[serde(skip)] + pub rotations: Option>>, + #[serde(skip)] + pub model_matrix_storage: Option, + // TODO: Consider splitting up the FE model components into several components, + // since some components can be re-used across 2d and 3d (material, u, v, static_nodes), + // and it might anyway be convenient to store the material volume/surface meshes + // as optional components of the respective volume mesh/contact geometry components. +} + +impl FiniteElementElasticModel3d { + pub fn ensure_model_matrix_storage_initialized(&mut self, dirichlet_bcs: Option<&dyn DirichletBoundaryConditions>) { + if self.model_matrix_storage.is_none() { + let mass_matrix = { + profile!("assemble mass matrix"); + let mut mass_matrix = self + .model + .assemble_mass(self.material.density) + .to_csr(Add::add); + apply_homogeneous_dirichlet_bc_csr::(&mut mass_matrix, dirichlet_bcs.nodes()); + mass_matrix + }; + info!( + "Assembled mass matrix ({} x {}): {} nnz", + mass_matrix.nrows(), + mass_matrix.ncols(), + mass_matrix.nnz() + ); + + let representative_force = compute_representative_force(&mass_matrix, 3); + self.model_matrix_storage = Some(ElasticModelMatrixStorage::new_from_mass( + mass_matrix, + representative_force, + )); + } + } +} + +impl Component for FiniteElementElasticModel3d { + type Storage = VecStorage; +} + +impl ElasticityModel for FiniteElementModel2d { + fn ndof(&self) -> usize { + match_on_finite_element_model_2d!(self, model => model.ndof()) + } + + fn assemble_stiffness_into( + &self, + csr: &mut CsrMatrix, + u: &DVector, + material_model: &dyn solid::ElasticMaterialModel, + ) { + match_on_finite_element_model_2d!(self, + model => model.assemble_stiffness_into(csr, u, material_model)) + } + + fn assemble_stiffness( + &self, + u: &DVector, + material_model: &dyn solid::ElasticMaterialModel, + ) -> CooMatrix { + match_on_finite_element_model_2d!(self, + model => model.assemble_stiffness(u, material_model)) + } + + fn assemble_mass(&self, density: f64) -> CooMatrix { + match_on_finite_element_model_2d!(self, + model => model.assemble_mass(density)) + } + + fn assemble_elastic_pseudo_forces( + &self, + u: DVectorSlice, + material_model: &dyn solid::ElasticMaterialModel, + ) -> DVector { + match_on_finite_element_model_2d!(self, + model => model.assemble_elastic_pseudo_forces(u, material_model)) + } + + fn compute_scalar_element_integrals( + &self, + u: DVectorSlice, + integrand: &dyn ScalarMaterialSpaceFunction, + ) -> DVector { + match_on_finite_element_model_2d!(self, + model => model.compute_scalar_element_integrals(u, integrand)) + } +} + +impl ElasticityModel for FiniteElementModel3d { + fn ndof(&self) -> usize { + match_on_finite_element_model_3d!(self, model => model.ndof()) + } + + fn assemble_stiffness_into( + &self, + csr: &mut CsrMatrix, + u: &DVector, + material_model: &dyn solid::ElasticMaterialModel, + ) { + match_on_finite_element_model_3d!(self, + model => model.assemble_stiffness_into(csr, u, material_model)) + } + + fn assemble_stiffness( + &self, + u: &DVector, + material_model: &dyn solid::ElasticMaterialModel, + ) -> CooMatrix { + match_on_finite_element_model_3d!(self, + model => model.assemble_stiffness(u, material_model)) + } + + fn assemble_mass(&self, density: f64) -> CooMatrix { + match_on_finite_element_model_3d!(self, + model => model.assemble_mass(density)) + } + + fn assemble_elastic_pseudo_forces( + &self, + u: DVectorSlice, + material_model: &dyn solid::ElasticMaterialModel, + ) -> DVector { + match_on_finite_element_model_3d!(self, + model => model.assemble_elastic_pseudo_forces(u, material_model)) + } + + fn compute_scalar_element_integrals( + &self, + u: DVectorSlice, + integrand: &dyn ScalarMaterialSpaceFunction, + ) -> DVector { + match_on_finite_element_model_3d!(self, + model => model.compute_scalar_element_integrals(u, integrand)) + } +} + +impl ElasticityModelParallel for FiniteElementModel2d { + fn assemble_elastic_pseudo_forces_into_par( + &self, + f: DVectorSliceMut, + u: DVectorSlice, + material_model: &(dyn Sync + solid::ElasticMaterialModel), + ) { + match_on_finite_element_model_2d!(self, + model => model.assemble_elastic_pseudo_forces_into_par(f, u, material_model)) + } + + fn assemble_transformed_stiffness_par( + &self, + u: &DVector, + material_model: &(dyn Sync + solid::ElasticMaterialModel), + transformation: &(dyn Sync + ElementMatrixTransformation), + ) -> CooMatrix { + match_on_finite_element_model_2d!(self, + model => model.assemble_transformed_stiffness_par(u, material_model, transformation)) + } + + fn assemble_transformed_stiffness_into_par( + &self, + csr: &mut CsrMatrix, + u: &DVector, + material_model: &(dyn Sync + solid::ElasticMaterialModel), + transformation: &(dyn Sync + ElementMatrixTransformation), + ) { + match_on_finite_element_model_2d!(self, + model => model.assemble_transformed_stiffness_into_par( + csr, u, material_model, transformation)) + } + + fn compute_scalar_element_integrals_par( + &self, + u: DVectorSlice, + integrand: &(dyn Sync + ScalarMaterialSpaceFunction), + ) -> DVector { + match_on_finite_element_model_2d!(self, + model => model.compute_scalar_element_integrals_par(u, integrand)) + } +} + +impl ElasticityModelParallel for FiniteElementModel3d { + fn assemble_elastic_pseudo_forces_into_par( + &self, + f: DVectorSliceMut, + u: DVectorSlice, + material_model: &(dyn Sync + solid::ElasticMaterialModel), + ) { + match_on_finite_element_model_3d!(self, + model => model.assemble_elastic_pseudo_forces_into_par(f, u, material_model)) + } + + fn assemble_transformed_stiffness_par( + &self, + u: &DVector, + material_model: &(dyn Sync + solid::ElasticMaterialModel), + transformation: &(dyn Sync + ElementMatrixTransformation), + ) -> CooMatrix { + match_on_finite_element_model_3d!(self, + model => model.assemble_transformed_stiffness_par(u, material_model, transformation)) + } + + fn assemble_transformed_stiffness_into_par( + &self, + csr: &mut CsrMatrix, + u: &DVector, + material_model: &(dyn Sync + solid::ElasticMaterialModel), + transformation: &(dyn Sync + ElementMatrixTransformation), + ) { + match_on_finite_element_model_3d!(self, + model => model.assemble_transformed_stiffness_into_par( + csr, u, material_model, transformation)) + } + + fn compute_scalar_element_integrals_par( + &self, + u: DVectorSlice, + integrand: &(dyn Sync + ScalarMaterialSpaceFunction), + ) -> DVector { + match_on_finite_element_model_3d!(self, + model => model.compute_scalar_element_integrals_par(u, integrand)) + } +} diff --git a/simulation_toolbox/src/fem/integrator.rs b/simulation_toolbox/src/fem/integrator.rs new file mode 100644 index 0000000..2a33af2 --- /dev/null +++ b/simulation_toolbox/src/fem/integrator.rs @@ -0,0 +1,807 @@ +use std::error::Error; +use std::fmt; +use std::fmt::Display; + +use coarse_prof::profile; +use fenris::assembly::apply_homogeneous_dirichlet_bc_rhs; +use fenris::nalgebra::allocator::Allocator; +use fenris::nalgebra::{DVector, DVectorSlice, DVectorSliceMut, DefaultAllocator, DimName, U4}; +use fenris::solid::ElasticityModelParallel; +use fenris::sparse::spmv_csr; +use fenris::{solid, CsrMatrix}; +use global_stash::stash_scope; +use hamilton::{BijectiveStorage, StorageContainer, System}; +use hamilton2::dynamic_system::{DifferentiableDynamicSystem, DynamicSystem}; +use hamilton2::integrators::{backward_euler_step, symplectic_euler_step, BackwardEulerSettings}; +use log::{info, warn}; +use mkl_corrode::dss; +use mkl_corrode::dss::Definiteness; +use mkl_corrode::dss::MatrixStructure::Symmetric; + +use crate::components::{get_gravity, get_simulation_time, get_time_step, Gravity, Name}; +use crate::fem::system_assembly::*; +use crate::fem::{ + DirichletBoundaryConditionComponent, DirichletBoundaryConditions, ElasticModelMatrixStorage, + FiniteElementElasticModel2d, FiniteElementElasticModel3d, IntegrationMethod, OptionalDirichletBoundaryConditions, +}; +use crate::util::IfTrue; + +#[derive(Debug, Clone)] +pub struct IntegratorSettings { + project_stiffness: bool, +} + +impl Default for IntegratorSettings { + fn default() -> Self { + Self { + project_stiffness: true, + } + } +} + +impl IntegratorSettings { + pub fn project_stiffness(&self) -> bool { + self.project_stiffness + } + + pub fn set_project_stiffness(mut self, project: bool) -> Self { + self.project_stiffness = project; + self + } +} + +#[derive(Debug, Default)] +pub struct FiniteElementIntegrator { + integrator2d: FiniteElementIntegrator2d, + integrator3d: FiniteElementIntegrator3d, +} + +#[derive(Debug, Default)] +pub struct FiniteElementIntegrator2d { + settings: IntegratorSettings, +} + +#[derive(Debug, Default)] +pub struct FiniteElementIntegrator3d { + settings: IntegratorSettings, +} + +impl FiniteElementIntegrator { + pub fn with_settings(settings: IntegratorSettings) -> Self { + Self { + integrator2d: FiniteElementIntegrator2d::with_settings(settings.clone()), + integrator3d: FiniteElementIntegrator3d::with_settings(settings), + } + } +} + +impl FiniteElementIntegrator2d { + pub fn with_settings(settings: IntegratorSettings) -> Self { + Self { settings } + } +} + +impl FiniteElementIntegrator3d { + pub fn with_settings(settings: IntegratorSettings) -> Self { + Self { settings } + } +} + +impl Display for FiniteElementIntegrator { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "FiniteElementIntegrator") + } +} + +impl Display for FiniteElementIntegrator2d { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "FiniteElementIntegrator2d") + } +} + +impl Display for FiniteElementIntegrator3d { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "FiniteElementIntegrator3d") + } +} + +/// Integrates a finite element model by recursively substepping upon failures. +fn recursively_substep_finite_element_model<'a, D, Model>( + model: &Model, + num_substeps: usize, + initial_t: f64, + initial_dt: f64, + u: impl Into>, + v: impl Into>, + dirichlet_bcs: Option<&'a dyn DirichletBoundaryConditions>, + matrices: &mut ElasticModelMatrixStorage, + solver: &mut Option>, + mass_damping_coefficient: Option, + stiffness_damping_coefficient: Option, + material_model: &(dyn Sync + solid::ElasticMaterialModel), + integrator: &IntegrationMethod, + gravity: Option<&Gravity>, + project_stiffness: bool, +) -> Result<(), Box> +where + Model: ElasticityModelParallel, + D: DimName, + DefaultAllocator: Allocator + Allocator, +{ + // If the number of substeps in a recursion exceeds this number, we abort + let max_substeps = 1000; + // Number of substeps to take when re-trying with a smaller time step + let num_substeps_per_level = 5; + + let mut u = u.into(); + let mut v = v.into(); + + // TODO: We're currently cloning this upon every recursion, but this is not necessary + // (it never changes throughout all of the substepping) + let u_initial = u.clone_owned(); + let v_initial = v.clone_owned(); + + let dt = initial_dt / (num_substeps as f64); + for step_idx in 0..num_substeps { + let t = initial_t + (step_idx as f64) * dt; + if let Err(err) = integrate_finite_element_model( + model, + t, + dt, + &mut u, + &mut v, + dirichlet_bcs, + matrices, + solver, + mass_damping_coefficient, + stiffness_damping_coefficient, + material_model, + integrator, + gravity, + project_stiffness, + ) { + let new_num_substeps = num_substeps * num_substeps_per_level; + if new_num_substeps > max_substeps { + return Err(Box::from(format!( + "Substepping failed.\ + Could not find solution using {} substeps. Latest error from integrator:\ + {}", + num_substeps, err + ))); + } else { + warn!( + "Integrator failed. Attempting to substep with {} substeps", + new_num_substeps + ); + // Reset state to initial state given at the start of the time step. + u.copy_from(&u_initial); + v.copy_from(&v_initial); + + recursively_substep_finite_element_model( + model, + new_num_substeps, + initial_t, + initial_dt, + &mut u, + &mut v, + dirichlet_bcs, + matrices, + solver, + mass_damping_coefficient, + stiffness_damping_coefficient, + material_model, + integrator, + gravity, + project_stiffness, + )?; + } + } + } + + assert!(u.iter().all(|u_i| u_i.is_finite())); + assert!(v.iter().all(|v_i| v_i.is_finite())); + + Ok(()) +} + +fn integrate_finite_element_model<'a, D, Model>( + model: &Model, + t: f64, + dt: f64, + u: impl Into>, + v: impl Into>, + dirichlet_bcs: Option<&'a dyn DirichletBoundaryConditions>, + matrices: &mut ElasticModelMatrixStorage, + solver: &mut Option>, + mass_damping_coefficient: Option, + stiffness_damping_coefficient: Option, + material_model: &(dyn Sync + solid::ElasticMaterialModel), + integrator: &IntegrationMethod, + gravity: Option<&Gravity>, + project_stiffness: bool, +) -> Result<(), Box> +where + Model: ElasticityModelParallel, + D: DimName, + DefaultAllocator: Allocator + Allocator + Allocator + Allocator, +{ + let mut u = u.into(); + let mut v = v.into(); + + let g_force = if let Some(gravity) = gravity { + let g_force_density = compute_gravity_density(model, gravity)?; + &matrices.mass_matrix * &g_force_density + } else { + DVector::zeros(model.ndof()) + }; + + matrices.has_damping_matrix = compute_damping_matrix_into( + model, + DVectorSlice::from(&u), + &mut matrices.stiffness_matrix, + &mut matrices.damping_matrix, + &matrices.mass_matrix, + mass_damping_coefficient, + stiffness_damping_coefficient, + material_model, + ); + + let representative_force = matrices.representative_force; + + let mut system = FiniteElementElastodynamicSystem { + model, + dirichlet_bcs, + + matrices, + solver, + + material_model, + g_force: &g_force, + project_stiffness, + + state: None, + jacobian_apply_params: None, + jacobian_solve_params: None, + + stiffness_matrix_outdated: true, + inverse_mass_matrix_outdated: true, + solve_jacobian_combination_outdated: true, + apply_jacobian_combination_outdated: true, + }; + + let tolerance = 1e-5 * representative_force; + let max_newton_iter = Some(100); + + { + let u = DVectorSliceMut::from(&mut u); + let v = DVectorSliceMut::from(&mut v); + match integrator { + IntegrationMethod::SymplecticEuler => { + profile!("symplectic euler"); + let nrows = u.nrows(); + symplectic_euler_step( + &mut system, + u, + v, + &mut DVector::zeros(nrows), + &mut DVector::zeros(nrows), + t, + dt, + )?; + } + IntegrationMethod::BackwardEuler => { + let integrator_settings = BackwardEulerSettings { + max_newton_iter, + tolerance, + }; + + profile!("backward euler"); + stash_scope!("backward euler"); + let newton_iter = backward_euler_step(&mut system, u, v, t, dt, integrator_settings)?; + info!("Number of newton iterations in Backward Euler step: {}", newton_iter); + global_stash::insert_value_or_modify("newton_iter", newton_iter, |accumulated_iter| { + let current_iter = accumulated_iter.as_u64().unwrap(); + *accumulated_iter = (current_iter + newton_iter as u64).into(); + }); + } + } + } + + apply_dirichlet_bcs_unknowns(t + dt, u, v, dirichlet_bcs); + + Ok(()) +} + +impl System for FiniteElementIntegrator2d { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + let dt = get_time_step(data)?; + let t = get_simulation_time(data)?; + let g = get_gravity(data)?; + + let mut models_2d = data + .get_component_storage::() + .borrow_mut(); + + let bcs = data + .get_component_storage::() + .borrow(); + + for (id, model) in models_2d.entity_component_iter_mut() { + let model_bcs = bcs + .get_component_for_entity(*id) + .map(|bc_comp| &*bc_comp.bc); + + model.ensure_model_matrix_storage_initialized(model_bcs); + // TODO: This should only be necessary on the very first timestep, to make initial conditions consistent + apply_dirichlet_bcs_unknowns(t, &mut model.u, &mut model.v, model_bcs); + + let g = model.gravity_enabled.if_true(&g); + + match_on_elastic_material_model!(model.material.elastic_model, material_model => { + // TODO: Make this configurable also for 2D + recursively_substep_finite_element_model( + &model.model, + 1, + t, + dt, + &mut model.u, + &mut model.v, + model_bcs, + model.model_matrix_storage.as_mut().expect("ElasticModelMatrixStorage was not initialized"), + &mut model.factorization, + model.material.mass_damping_coefficient, + model.material.stiffness_damping_coefficient, + material_model, + &model.integrator, + g, + self.settings.project_stiffness() + )?; + }); + } + + Ok(()) + } +} + +impl System for FiniteElementIntegrator3d { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + let dt = get_time_step(data)?; + let t = get_simulation_time(data)?; + let g = get_gravity(data)?; + + let mut models_3d = data + .get_component_storage::() + .borrow_mut(); + + let bcs = data + .get_component_storage::() + .borrow(); + + let name_storage = data.get_component_storage::().borrow(); + + for (id, model) in models_3d.entity_component_iter_mut() { + let model_bcs = bcs + .get_component_for_entity(*id) + .map(|bc_comp| &*bc_comp.bc); + + let scope_name = if let Some(name) = name_storage.get_component(*id) { + name.0.clone() + } else { + format!("{:?}", *id) + }; + stash_scope!(scope_name); + + model.ensure_model_matrix_storage_initialized(model_bcs); + // TODO: This should only be necessary on the very first timestep, to make initial conditions consistent + apply_dirichlet_bcs_unknowns(t, &mut model.u, &mut model.v, model_bcs); + + let g = if model.gravity_enabled { + // TODO: Add a EntityGravityComponent + Some(&g) + } else { + None + }; + + match_on_elastic_material_model!(model.material.elastic_model, material_model => { + recursively_substep_finite_element_model( + &model.model, + 1, + t, + dt, + &mut model.u, + &mut model.v, + model_bcs, + model.model_matrix_storage.as_mut().expect("ElasticModelMatrixStorage was not initialized"), + &mut model.factorization, + model.material.mass_damping_coefficient, + model.material.stiffness_damping_coefficient, + material_model, + &model.integrator, + g, + self.settings.project_stiffness() + )?; + }); + } + + Ok(()) + } +} + +impl System for FiniteElementIntegrator { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + self.integrator2d.run(data)?; + self.integrator3d.run(data)?; + Ok(()) + } +} + +#[derive(Clone, Debug)] +struct State { + t: f64, + u: DVector, + v: DVector, +} + +// TODO: Clean up this mess, +struct FiniteElementElastodynamicSystem<'a, D: DimName> +where + DefaultAllocator: Allocator, +{ + model: &'a dyn ElasticityModelParallel, + dirichlet_bcs: Option<&'a dyn DirichletBoundaryConditions>, + + matrices: &'a mut ElasticModelMatrixStorage, + solver: &'a mut Option>, + + material_model: &'a (dyn Sync + solid::ElasticMaterialModel), + g_force: &'a DVector, + + project_stiffness: bool, + + /// The current state of the dynamic system (t, u, v) + state: Option, + /// Coefficients (alpha, beta, gamma) for the apply_jacobian_combination method + jacobian_apply_params: Option<(Option, Option, Option)>, + /// Coefficients (alpha, beta) for the solve_jacobian_combination method + jacobian_solve_params: Option<(Option, Option)>, + + /// If the stiffness matrix is outdated, everything that depends on it should be recomputed + stiffness_matrix_outdated: bool, + /// If the inverse mass matrix factorization is outdated, it has to be recomputed before applying it + inverse_mass_matrix_outdated: bool, + /// If the cached Jacobian combination is outdated, it should be recomputed before applying it + apply_jacobian_combination_outdated: bool, + /// If the solver is outdated, a numerical refactorization has to be performed before solving + solve_jacobian_combination_outdated: bool, +} + +impl<'a, D: DimName> FiniteElementElastodynamicSystem<'a, D> +where + DefaultAllocator: Allocator + Allocator, +{ + /// Computes a new stiffness matrix from the currently cached state without checking if it is outdated + fn update_stiffness_matrix(&mut self) -> Result<(), Box> { + let state = self + .state + .as_ref() + .ok_or_else(|| Box::::from("No state was set using `set_state`"))?; + + compute_stiffness_matrix_into( + self.model, + state.t, + DVectorSlice::from(&state.u), + DVectorSlice::from(&state.v), + &mut self.matrices.stiffness_matrix, + self.dirichlet_bcs, + self.material_model, + self.project_stiffness, + ); + + self.stiffness_matrix_outdated = false; + + // The Jacobian operators are now outdated after updating the stiffness matrix + self.apply_jacobian_combination_outdated = true; + self.solve_jacobian_combination_outdated = true; + + Ok(()) + } + + /// Computes a new stiffness matrix if it is outdated + fn ensure_stiffness_matrix_updated(&mut self) -> Result<(), Box> { + if self.stiffness_matrix_outdated { + self.update_stiffness_matrix()? + } + + Ok(()) + } + + /// Computes a new mass matrix factorization if it is outdated + fn ensure_inverse_mass_matrix_updated(&mut self) -> Result<(), Box> { + // TODO: Maybe this should use a separate factorization? Are there integrators that use both + // an inverse mass matrix and a solve_jacobian_combination call? + + if self.inverse_mass_matrix_outdated { + profile!("refactorize mass matrix"); + + Self::refactorize(self.solver, &self.matrices.mass_matrix)?; + self.inverse_mass_matrix_outdated = false; + + // The jacobian factorization was replaced by the mass matrix factorization + self.solve_jacobian_combination_outdated = true; + } + + Ok(()) + } + + /// Computes a new Jacobian combination if it is outdated + fn ensure_jacobian_combination_apply_updated(&mut self) -> Result<(), Box> { + self.ensure_stiffness_matrix_updated()?; + + if self.apply_jacobian_combination_outdated { + profile!("compute jacobian linear combination"); + let (alpha, beta, gamma) = self.jacobian_apply_params.clone().ok_or_else(|| { + Box::::from( + "No (alpha, beta, gamma) parameters were set using `init_apply_jacobian_combination`", + ) + })?; + + compute_jacobian_combination_into( + &mut self.matrices.linear_combination_apply, + &self.matrices.stiffness_matrix, + self.matrices + .has_damping_matrix + .if_true(&self.matrices.damping_matrix), + &self.matrices.mass_matrix, + alpha, + beta, + gamma, + self.dirichlet_bcs, + ); + + self.apply_jacobian_combination_outdated = false; + } + + Ok(()) + } + + /// Computes a new Jacobian factorization if it is outdated + fn ensure_jacobian_combination_solve_updated(&mut self) -> Result<(), Box> { + self.ensure_stiffness_matrix_updated()?; + + if self.solve_jacobian_combination_outdated { + profile!("refactorize jacobian linear combination"); + + let (alpha, beta) = self.jacobian_solve_params.clone().ok_or_else(|| { + Box::::from("No (alpha, beta) parameters were set using `init_solve_jacobian_combination`") + })?; + assert!(-alpha.unwrap_or(0.0) > 0.0); + + // Compute the requested Jacobian combination to factorize + compute_jacobian_combination_into( + &mut self.matrices.linear_combination_solve, + &self.matrices.stiffness_matrix, + self.matrices + .has_damping_matrix + .if_true(&self.matrices.damping_matrix), + &self.matrices.mass_matrix, + alpha, + beta, + Some(1.0), + self.dirichlet_bcs, + ); + + Self::refactorize(self.solver, &self.matrices.linear_combination_solve)?; + self.solve_jacobian_combination_outdated = false; + + // The mass matrix factorization was replaced by this factorization + self.inverse_mass_matrix_outdated = true; + } + + Ok(()) + } + + /// Perform a factorization of the specified matrix in the cached solver + fn refactorize(solver: &mut Option>, matrix: &CsrMatrix) -> Result<(), Box> { + // Perform factorization + let new_dss_solver = { + profile!("factorization"); + let solver_status = if let Some(mut solver) = solver.take() { + profile!("numerical refactorization"); + // MKL requires the non-zeros corresponding to the upper triangular + // part of the matrix + let mut upper_triangular_values = Vec::with_capacity(matrix.nnz() / 2 + matrix.nrows()); + upper_triangular_values.extend(matrix.iter().filter(|(i, j, _)| j >= i).map(|(_, _, v)| *v)); + solver + .refactor(&upper_triangular_values, Definiteness::PositiveDefinite) + .map(|_| solver) + } else { + profile!("full factorization"); + // MKL only accepts a triangular part of the matrix, so we must (unfortunately) + // first convert the matrix into something DSS accepts, even if we just want to refactor. + let matrix_dss = dss::SparseMatrix::try_convert_from_csr( + matrix.row_offsets(), + matrix.column_indices(), + matrix.values(), + Symmetric, + )?; + let options = dss::SolverOptions::default().parallel_reorder(true); + dss::Solver::try_factor_with_opts(&matrix_dss, Definiteness::PositiveDefinite, &options) + }; + + match solver_status { + Ok(solver) => solver, + Err(e) => { + match e.return_code() { + // Usually this error indicates that the Jacobian is not positive definite + dss::ErrorCode::TermLvlErr => { + profile!("indefinite factorization"); + warn!( + "Warning: Jacobian does not seem to be positive definite. \ + Using indefinite factorization." + ); + // Try to factorize again with indefinite factorization + let jacobian_dss = dss::SparseMatrix::try_convert_from_csr( + matrix.row_offsets(), + matrix.column_indices(), + matrix.values(), + Symmetric, + )?; + let options = dss::SolverOptions::default().parallel_reorder(true); + dss::Solver::try_factor_with_opts(&jacobian_dss, Definiteness::Indefinite, &options)? + } + _ => return Err(e.into()), + } + } + } + }; + + solver.replace(new_dss_solver); + + Ok(()) + } +} + +impl<'a, D> DynamicSystem for FiniteElementElastodynamicSystem<'a, D> +where + D: DimName, + DefaultAllocator: Allocator + Allocator, +{ + fn apply_mass_matrix(&mut self, mut y: DVectorSliceMut, x: DVectorSlice) { + profile!("apply mass matrix"); + spmv_csr(1.0, &mut y, 1.0, &self.matrices.mass_matrix, &x); + } + + fn apply_inverse_mass_matrix( + &mut self, + mut y: DVectorSliceMut, + x: DVectorSlice, + ) -> Result<(), Box> { + self.ensure_inverse_mass_matrix_updated()?; + + { + profile!("apply inverse mass"); + + let solution = { + profile!("solve"); + self.solver.as_mut().unwrap().solve(x.as_slice())? + }; + + y.copy_from_slice(solution.as_slice()); + } + + Ok(()) + } + + fn eval_f( + &mut self, + mut f: DVectorSliceMut, + t: f64, + u: DVectorSlice, + v: DVectorSlice, + ) -> Result<(), Box> { + profile!("eval f"); + + // TODO: Avoid copy? + let mut u = u.clone_owned(); + let mut v = v.clone_owned(); + + // Apply boundary conditions to unknowns + apply_dirichlet_bcs_unknowns( + t, + DVectorSliceMut::from(&mut u), + DVectorSliceMut::from(&mut v), + self.dirichlet_bcs, + ); + + // Compute elastic forces + f.fill(0.0); + self.model.assemble_elastic_pseudo_forces_into_par( + DVectorSliceMut::from(&mut f), + DVectorSlice::from(&u), + self.material_model, + ); + // Apply gravity + f += self.g_force; + // Apply damping + if self.matrices.has_damping_matrix { + spmv_csr(1.0, &mut f, -1.0, &self.matrices.damping_matrix, &v); + } + // Apply boundary conditions + apply_homogeneous_dirichlet_bc_rhs(f, self.dirichlet_bcs.nodes(), D::dim()); + + Ok(()) + } +} + +impl<'a, D> DifferentiableDynamicSystem for FiniteElementElastodynamicSystem<'a, D> +where + D: DimName, + DefaultAllocator: Allocator + Allocator, +{ + fn set_state(&mut self, t: f64, u: DVectorSlice, v: DVectorSlice) -> Result<(), Box> { + self.state.replace(State { + t, + u: u.clone_owned(), + v: v.clone_owned(), + }); + + // Updating the state invalidates all cached operators (except for inverse mass matrix) + self.stiffness_matrix_outdated = true; + self.apply_jacobian_combination_outdated = true; + self.solve_jacobian_combination_outdated = true; + Ok(()) + } + + fn init_apply_jacobian_combination( + &mut self, + alpha: Option, + beta: Option, + gamma: Option, + ) -> Result<(), Box> { + self.jacobian_apply_params.replace((alpha, beta, gamma)); + + self.apply_jacobian_combination_outdated = true; + Ok(()) + } + + fn init_solve_jacobian_combination(&mut self, alpha: Option, beta: Option) -> Result<(), Box> { + self.jacobian_solve_params.replace((alpha, beta)); + + self.solve_jacobian_combination_outdated = true; + Ok(()) + } + + fn apply_jacobian_combination( + &mut self, + mut y: DVectorSliceMut, + x: DVectorSlice, + ) -> Result<(), Box> { + self.ensure_jacobian_combination_apply_updated()?; + + { + profile!("apply jacobian combination"); + spmv_csr(1.0, &mut y, 1.0, &self.matrices.linear_combination_apply, &x); + Ok(()) + } + } + + fn solve_jacobian_combination( + &mut self, + mut sol: DVectorSliceMut, + rhs: DVectorSlice, + ) -> Result<(), Box> { + self.ensure_jacobian_combination_solve_updated()?; + + { + profile!("solve jacobian combination"); + + let solution = { + profile!("solve"); + self.solver.as_mut().unwrap().solve(rhs.as_slice())? + }; + + sol.copy_from_slice(solution.as_slice()); + Ok(()) + } + } +} diff --git a/simulation_toolbox/src/fem/mod.rs b/simulation_toolbox/src/fem/mod.rs new file mode 100644 index 0000000..711787c --- /dev/null +++ b/simulation_toolbox/src/fem/mod.rs @@ -0,0 +1,32 @@ +#[macro_use] +mod fe_model; +pub use fe_model::*; + +mod deformer; +pub use deformer::*; + +mod system_assembly; + +mod integrator; +pub use integrator::*; + +pub mod newton_cg; + +pub mod bcs; +pub use bcs::{DirichletBoundaryConditionComponent, DirichletBoundaryConditions, OptionalDirichletBoundaryConditions}; + +pub mod schwarz_precond; + +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub enum IntegrationMethod { + SymplecticEuler, + BackwardEuler, +} + +impl Default for IntegrationMethod { + fn default() -> Self { + IntegrationMethod::BackwardEuler + } +} diff --git a/simulation_toolbox/src/fem/newton_cg.rs b/simulation_toolbox/src/fem/newton_cg.rs new file mode 100644 index 0000000..86d4eff --- /dev/null +++ b/simulation_toolbox/src/fem/newton_cg.rs @@ -0,0 +1,877 @@ +use crate::components::{get_gravity, get_simulation_time, get_time_step, Gravity}; +use crate::fem::schwarz_precond::{ + build_preconditioner_pattern, build_schwarz_preconditioner_into, SchwarzPreconditionerComponent, +}; +use crate::fem::system_assembly::{ + apply_dirichlet_bcs_unknowns, compute_damping_matrix_into, compute_gravity_density, + compute_jacobian_combination_into, compute_stiffness_matrix_into, +}; +use crate::fem::{ + DirichletBoundaryConditionComponent, DirichletBoundaryConditions, ElasticModelMatrixStorage, + FiniteElementElasticModel3d, OptionalDirichletBoundaryConditions, +}; +use coarse_prof::profile; +use fenris::assembly::apply_homogeneous_dirichlet_bc_rhs; +use fenris::cg::{CgWorkspace, ConjugateGradient, LinearOperator, RelativeResidualCriterion}; +use fenris::nalgebra::{DVector, DVectorSlice, DVectorSliceMut, Dynamic, Matrix3, VecStorage, U1, U3}; +use fenris::nested_vec::NestedVec; +use fenris::solid::{ElasticMaterialModel, ElasticityModelParallel}; +use fenris::sparse::{spmv_csr, SparsityPattern}; +use fenris::CsrMatrix; +use hamilton::{BijectiveStorage, BijectiveStorageMut, StorageContainer, System}; +use hamilton2::dynamic_system::{DifferentiableDynamicSystem, DynamicSystem}; +use hamilton2::integrators::{backward_euler_step, BackwardEulerSettings}; +use log::{info, warn}; +use mkl_corrode::mkl_sys::MKL_INT; +use mkl_corrode::sparse::{CsrMatrixHandle, MatrixDescription, SparseOperation}; +use paradis::DisjointSubsets; +use std::convert::TryFrom; +use std::error::Error; +use std::fmt; +use std::ops::{Deref, DerefMut}; +use std::pin::Pin; + +#[derive(Debug, Default)] +pub struct NewtonCgIntegrator3d {} + +impl fmt::Display for NewtonCgIntegrator3d { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", std::any::type_name::()) + } +} + +impl System for NewtonCgIntegrator3d { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + let dt = get_time_step(data)?; + let t = get_simulation_time(data)?; + let g = get_gravity(data)?; + + let mut models_3d = data + .get_component_storage::() + .borrow_mut(); + + let bcs = data + .get_component_storage::() + .borrow(); + + let mut schwarz_components = data + .get_component_storage::() + .borrow_mut(); + + for (id, model) in models_3d.entity_component_iter_mut() { + let model_bcs = bcs + .get_component_for_entity(*id) + .map(|bc_comp| &*bc_comp.bc); + + model.ensure_model_matrix_storage_initialized(model_bcs); + // TODO: This should only be necessary on the very first timestep, + // to make initial conditions consistent + apply_dirichlet_bcs_unknowns(t, &mut model.u, &mut model.v, model_bcs); + + let g = if model.gravity_enabled { + // TODO: Add a EntityGravityComponent + Some(&g) + } else { + None + }; + + let model_storage = model.model_matrix_storage.as_mut().unwrap(); + // TODO: Dont build workspace on every iteration + let mut workspace = NewtonCgWorkspace::build_workspace(model_storage.mass_matrix.nrows()); + workspace.prepare_workspace(model_storage.mass_matrix.nrows()); + + let schwarz_component = schwarz_components.get_component_for_entity_mut(*id); + let mut system_preconditioner = create_preconditioner(&model_storage.mass_matrix, schwarz_component)?; + + // Note: This match is absolutely necessary for avoiding overhead related to + // dynamic dispatch. + // TODO: Remove trait impl for ElasticMaterialModel component to avoid + // falling into this performance trap + match_on_elastic_material_model!(model.material.elastic_model, elastic_model => { + integrate_model_with_substeps( + model_storage, + elastic_model, + model_bcs, g, &model.model, + t, dt, + model.material.mass_damping_coefficient, + model.material.stiffness_damping_coefficient, + (&mut model.u).into(), (&mut model.v).into(), + &mut system_preconditioner, + &mut workspace)?; + }); + } + + Ok(()) + } +} + +fn create_preconditioner<'a>( + mass_matrix: &CsrMatrix, + schwarz_component: Option<&'a mut SchwarzPreconditionerComponent>, +) -> Result, Box> { + if let Some(schwarz_component) = schwarz_component { + if schwarz_component.preconditioner.is_none() { + let num_nodes = mass_matrix.nrows() / 3; + let pattern = build_preconditioner_pattern( + num_nodes, + &schwarz_component.schwarz_connectivity, + &schwarz_component.untreated_nodes, + 3, + ); + let nnz = pattern.nnz(); + let csr = CsrMatrix::from_pattern_and_values(pattern, vec![0.0; nnz]); + schwarz_component.preconditioner.replace(csr); + } + + let precond = SchwarzPreconditioner { + matrix: schwarz_component.preconditioner.as_mut().unwrap(), + schwarz_connectivity: &schwarz_component.schwarz_connectivity, + untreated_nodes: &schwarz_component.untreated_nodes, + colors: schwarz_component + .colors + .as_ref() + .ok_or_else(|| Box::::from("Missing coloring for Schwarz preconditioner"))?, + mkl_matrix: None, + }; + + Ok(Box::new(precond)) + } else { + // We don't have a Schwarz component, so fall back to diagonal preconditioning + // The preconditioner will anyway be updated, so we just create a default one for now + let diagonal_preconditioner = DiagonalPreconditioner::default(); + Ok(Box::new(diagonal_preconditioner)) + } +} + +fn integrate_model_with_substeps<'a>( + model_matrix_storage: &mut ElasticModelMatrixStorage, + material_model: &(dyn Sync + ElasticMaterialModel), + dirichlet_bc: Option<&dyn DirichletBoundaryConditions>, + gravity: Option<&Gravity>, + fe_model: &dyn ElasticityModelParallel, + t: f64, + dt: f64, + mass_damping_coefficient: Option, + stiffness_damping_coefficient: Option, + mut u: DVectorSliceMut, + mut v: DVectorSliceMut, + system_preconditioner: &'a mut (dyn SystemPreconditioner + 'a), + workspace: &mut NewtonCgWorkspace, +) -> Result<(), Box> { + // If the number of substeps in a recursion exceeds this number, we abort + let max_substeps = 1000; + // Multiply number of substeps by this factor every time there is a failure + let substep_factor = 5; + + // TODO: We're currently cloning this upon every recursion, but this is not necessary + // (it never changes throughout all of the substepping) + + let mut u_substep = u.clone_owned(); + let mut v_substep = v.clone_owned(); + + let mut num_substeps = 1; + let mut substep_dt = dt / (num_substeps as f64); + let mut substep_t; + + 'outer: loop { + for substep_idx in 0..num_substeps { + substep_t = t + substep_idx as f64 * substep_dt; + let substep_result = integrate_model( + model_matrix_storage, + material_model, + dirichlet_bc, + gravity, + fe_model, + substep_t, + substep_dt, + mass_damping_coefficient, + stiffness_damping_coefficient, + DVectorSliceMut::from(&mut u_substep), + DVectorSliceMut::from(&mut v_substep), + system_preconditioner, + workspace, + ); + if let Err(err) = substep_result { + // TODO: We currently substep no matter what the error was. Would be good to + // be able to distinguish recoverable errors from non-recoverable + num_substeps *= substep_factor; + + if num_substeps < max_substeps { + substep_dt = dt / (num_substeps as f64); + // Reset state to initial state + u_substep.copy_from(&u); + v_substep.copy_from(&v); + // Restart with new substep parameters + warn!( + "Integrator failed. Attempting to substep with {} substeps", + num_substeps + ); + continue 'outer; + } else { + return Err(Box::from(format!( + "Substepping failed.\ + Could not find solution using {} substeps. Latest error from integrator:\ + {}", + num_substeps, err + ))); + } + } + } + + // We successfully got through all substeps, so return result + u.copy_from(&u_substep); + v.copy_from(&v_substep); + assert!(u.iter().all(|u_i| u_i.is_finite())); + assert!(v.iter().all(|v_i| v_i.is_finite())); + return Ok(()); + } +} + +fn integrate_model<'a>( + model_matrix_storage: &mut ElasticModelMatrixStorage, + material_model: &(dyn Sync + ElasticMaterialModel), + dirichlet_bc: Option<&dyn DirichletBoundaryConditions>, + gravity: Option<&Gravity>, + fe_model: &dyn ElasticityModelParallel, + t: f64, + dt: f64, + mass_damping_coefficient: Option, + stiffness_damping_coefficient: Option, + mut u: DVectorSliceMut, + mut v: DVectorSliceMut, + system_preconditioner: &'a mut (dyn SystemPreconditioner + 'a), + workspace: &mut NewtonCgWorkspace, +) -> Result<(), Box> { + let mass_matrix = &model_matrix_storage.mass_matrix; + let n = mass_matrix.nrows(); + + let g_force = if let Some(gravity) = gravity { + let g_force_density = compute_gravity_density(fe_model, gravity)?; + mass_matrix * &g_force_density + } else { + DVector::zeros(n) + }; + // TODO: Avoid allocating gravity vector every time step + workspace.gravity_force.copy_from(&g_force); + model_matrix_storage.has_damping_matrix = compute_damping_matrix_into( + fe_model, + DVectorSlice::from(&u), + &mut model_matrix_storage.stiffness_matrix, + &mut model_matrix_storage.damping_matrix, + &model_matrix_storage.mass_matrix, + mass_damping_coefficient, + stiffness_damping_coefficient, + material_model, + ); + + let representative_force = model_matrix_storage.representative_force; + let mut system = NewtonCgDynamicSystem { + model_matrix_storage, + material_model, + model: fe_model, + bc: dirichlet_bc, + system_preconditioner, + workspace, + t, + alpha: None, + beta: None, + stiffness_matrix_outdated: true, + solve_jacobian_combination_outdated: true, + }; + + let settings = BackwardEulerSettings { + // TODO: Derive an appropriate tolerance + tolerance: 1e-5 * representative_force, + max_newton_iter: Some(100), + }; + { + profile!("backward euler"); + let newton_iter = backward_euler_step(&mut system, &mut u, &mut v, t, dt, settings)?; + info!("Number of newton iterations in Backward Euler step: {}", newton_iter); + } + + apply_dirichlet_bcs_unknowns(t + dt, &mut u, &mut v, dirichlet_bc); + Ok(()) +} + +pub struct SchwarzPreconditioner<'a> { + matrix: &'a mut CsrMatrix, + schwarz_connectivity: &'a NestedVec, + colors: &'a [DisjointSubsets], + untreated_nodes: &'a [usize], + mkl_matrix: Option>, +} + +struct MklMatrix<'a> { + connectivity: Pin>, + values: Pin>>, + handle: Option>, +} + +impl<'a> MklMatrix<'a> { + pub fn from_matrix<'b>(csr_matrix: &'b CsrMatrix) -> Self { + let mut mkl_matrix = MklMatrix { + connectivity: Box::pin(MklCsrConnectivity::from_csr_pattern(&csr_matrix.sparsity_pattern())), + values: Box::pin(csr_matrix.values().to_vec()), + handle: None, + }; + let connectivity_ptr = mkl_matrix.connectivity.deref() as *const MklCsrConnectivity; + let connectivity_ref = unsafe { &*connectivity_ptr }; + let values_ptr = mkl_matrix.values.deref() as *const Vec<_>; + let values_ref = unsafe { &*values_ptr }; + mkl_matrix.handle = Some(connectivity_ref.as_mkl_csr(values_ref.as_slice())); + + mkl_matrix + } + + pub fn handle(&self) -> &CsrMatrixHandle<'a, f64> { + self.handle.as_ref().unwrap() + } +} + +impl<'a> Drop for MklMatrix<'a> { + fn drop(&mut self) { + // Make sure that the handle gets dropped by removing the contents of the Option + self.handle.take(); + } +} + +impl<'a> LinearOperator for SchwarzPreconditioner<'a> { + fn apply(&self, y: DVectorSliceMut, x: DVectorSlice) -> Result<(), Box> { + let mkl_matrix = self + .mkl_matrix + .as_ref() + .ok_or_else(|| Box::::from("MKL Matrix has not yet been constructed"))?; + MklCsrLinearOperator { + csr: mkl_matrix.handle(), + } + .apply(y, x) + } +} + +impl<'a> SystemPreconditioner for SchwarzPreconditioner<'a> { + fn update(&mut self, system_matrix: &CsrMatrix) -> Result<(), Box> { + self.matrix.fill_par(0.0); + build_schwarz_preconditioner_into( + self.matrix, + system_matrix, + self.schwarz_connectivity, + self.untreated_nodes, + &self.colors, + 3, + )?; + self.mkl_matrix = Some(MklMatrix::from_matrix(&*self.matrix)); + Ok(()) + } + + fn as_linear_operator(&self) -> &dyn LinearOperator { + self + } +} + +#[derive(Debug)] +struct NewtonCgWorkspace { + gravity_force: DVector, + u: DVector, + v: DVector, + cg_workspace: CgWorkspace, +} + +impl NewtonCgWorkspace { + fn build_workspace(ndof: usize) -> Self { + Self { + gravity_force: DVector::zeros(ndof), + u: DVector::zeros(ndof), + v: DVector::zeros(ndof), + cg_workspace: CgWorkspace::default(), + } + } + + fn prepare_workspace(&mut self, ndof: usize) { + self.gravity_force.resize_vertically_mut(ndof, 0.0); + self.u.resize_vertically_mut(ndof, 0.0); + self.v.resize_vertically_mut(ndof, 0.0); + } +} + +pub trait SystemPreconditioner { + fn update(&mut self, system_matrix: &CsrMatrix) -> Result<(), Box>; + + fn as_linear_operator(&self) -> &dyn LinearOperator; +} + +impl<'a> SystemPreconditioner for Box { + fn update(&mut self, system_matrix: &CsrMatrix) -> Result<(), Box> { + self.deref_mut().update(system_matrix) + } + + fn as_linear_operator(&self) -> &dyn LinearOperator { + self.deref().as_linear_operator() + } +} + +struct NewtonCgDynamicSystem<'a> { + model_matrix_storage: &'a mut ElasticModelMatrixStorage, + material_model: &'a (dyn Sync + ElasticMaterialModel), + model: &'a dyn ElasticityModelParallel, + bc: Option<&'a dyn DirichletBoundaryConditions>, + system_preconditioner: &'a mut dyn SystemPreconditioner, + workspace: &'a mut NewtonCgWorkspace, + t: f64, + alpha: Option, + beta: Option, + stiffness_matrix_outdated: bool, + solve_jacobian_combination_outdated: bool, +} + +impl<'a> DynamicSystem for NewtonCgDynamicSystem<'a> { + fn apply_mass_matrix(&mut self, mut y: DVectorSliceMut, x: DVectorSlice) { + profile!("apply mass"); + spmv_csr(1.0, &mut y, 1.0, &self.model_matrix_storage.mass_matrix, &x); + } + + fn apply_inverse_mass_matrix( + &mut self, + _sol: DVectorSliceMut, + _rhs: DVectorSlice, + ) -> Result<(), Box> { + unimplemented!() + } + + fn eval_f( + &mut self, + mut f: DVectorSliceMut, + t: f64, + u: DVectorSlice, + v: DVectorSlice, + ) -> Result<(), Box> { + // f <- Mg + f.axpy(1.0, &self.workspace.gravity_force, 0.0); + + // TODO: Avoid copy? + let mut u = u.clone_owned(); + let mut v = v.clone_owned(); + + // Apply boundary conditions to unknowns + apply_dirichlet_bcs_unknowns(t, DVectorSliceMut::from(&mut u), DVectorSliceMut::from(&mut v), self.bc); + + // f += f_internal + { + profile!("assemble elastic forces"); + self.model.assemble_elastic_pseudo_forces_into_par( + DVectorSliceMut::from(&mut f), + DVectorSlice::from(&u), + self.material_model, + ); + } + + // f += -Dv + if self.model_matrix_storage.has_damping_matrix { + spmv_csr(1.0, &mut f, -1.0, &self.model_matrix_storage.damping_matrix, &v); + } + + // Apply boundary conditions + let dim = 3; + apply_homogeneous_dirichlet_bc_rhs(f, self.bc.nodes(), dim); + + Ok(()) + } +} + +pub struct BlockDiagonalPreconditioner3d { + blocks: Vec>, +} + +impl BlockDiagonalPreconditioner3d { + pub fn from_csr(matrix: &CsrMatrix) -> Result> { + assert_eq!(matrix.nrows(), matrix.ncols()); + assert_eq!(matrix.nrows() % 3, 0); + let num_blocks = matrix.nrows() / 3; + + let mut blocks = Vec::with_capacity(num_blocks); + + for block_idx in 0..num_blocks { + let mut block = Matrix3::zeros(); + + for i in 0..3 { + let row_idx = 3 * block_idx + i; + let csr_row = matrix.row(row_idx); + + let mut block_row = block.row_mut(i); + // TODO: This incurs three binary searches, which is unnecessarily expensive + block_row[(i + 3 - 1) % 3] = csr_row.get(3 * block_idx + (i + 3 - 1) % 3).unwrap_or(0.0); + block_row[(i + 3 - 0) % 3] = csr_row.get(3 * block_idx + (i + 3 - 0) % 3).unwrap_or(0.0); + block_row[(i + 3 + 1) % 3] = csr_row.get(3 * block_idx + (i + 3 + 1) % 3).unwrap_or(0.0); + } + + let block_inverse = block + .try_inverse() + .ok_or_else(|| Box::::from("Matrix has singular blocks"))?; + blocks.push(block_inverse); + } + + Ok(Self { blocks }) + } +} + +impl LinearOperator for BlockDiagonalPreconditioner3d { + fn apply(&self, mut y: DVectorSliceMut, x: DVectorSlice) -> Result<(), Box> { + assert_eq!(y.len(), x.len()); + assert_eq!(y.len() % 3, 0); + let num_blocks = y.len() / 3; + + for block_idx in 0..num_blocks { + let block = &self.blocks[block_idx]; + let mut y_block = y.fixed_rows_mut::(3 * block_idx); + let x_block = x.fixed_rows::(3 * block_idx); + y_block.gemv(1.0, &block, &x_block, 0.0); + } + + Ok(()) + } +} + +pub struct DiagonalPreconditioner { + diag: DVector, +} + +impl Default for DiagonalPreconditioner { + fn default() -> Self { + Self { + diag: DVector::zeros(0), + } + } +} + +impl DiagonalPreconditioner { + pub fn from_csr(matrix: &CsrMatrix) -> Result> { + let diag_entries: Vec<_> = matrix + .diag_iter() + .map(|d_i| { + if d_i != 0.0 { + Ok(d_i.recip()) + } else { + Err(Box::::from("Diagonal matrix is not invertible.")) + } + }) + .collect::>()?; + + let diag_storage = VecStorage::new(Dynamic::new(matrix.nrows()), U1, diag_entries); + Ok(Self { + diag: DVector::from_data(diag_storage), + }) + } +} + +impl SystemPreconditioner for DiagonalPreconditioner { + fn update(&mut self, system_matrix: &CsrMatrix) -> Result<(), Box> { + self.diag.resize_vertically_mut(system_matrix.nrows(), 0.0); + self.diag + .iter_mut() + .zip(system_matrix.diag_iter()) + .map(|(d_i, a_ii)| { + if a_ii != 0.0 { + *d_i = a_ii.recip(); + Ok(()) + } else { + Err(Box::::from("Diagonal matrix is not invertible.")) + } + }) + .collect::>() + } + + fn as_linear_operator(&self) -> &dyn LinearOperator { + self + } +} + +impl LinearOperator for DiagonalPreconditioner { + fn apply(&self, mut y: DVectorSliceMut, x: DVectorSlice) -> Result<(), Box> { + y.zip_zip_apply(&x, &self.diag, |_, x_i, d_i| d_i * x_i); + Ok(()) + } +} + +pub struct MklCsrLinearOperator<'a> { + csr: &'a CsrMatrixHandle<'a, f64>, +} + +impl<'a> LinearOperator for MklCsrLinearOperator<'a> { + fn apply(&self, mut y: DVectorSliceMut, x: DVectorSlice) -> Result<(), Box> { + profile!("MKL sparse matrix-vector multiply"); + assert_eq!(y.len(), x.len()); + assert_eq!(y.len(), self.csr.rows()); + assert_eq!(x.len(), self.csr.cols()); + + let description = MatrixDescription::default(); + mkl_corrode::sparse::spmv_csr( + SparseOperation::NonTranspose, + 1.0, + self.csr, + &description, + x.as_slice(), + 0.0, + y.as_mut_slice(), + ) + .map_err(|_| Box::::from("MKL error during sparse spmv"))?; + Ok(()) + } +} + +/// Helper struct to store connectivity of an MKL sparse matrix (needs different index type). +pub struct MklCsrConnectivity { + rows: usize, + cols: usize, + row_offsets: Vec, + column_indices: Vec, +} + +impl MklCsrConnectivity { + pub fn from_csr_pattern(pattern: &SparsityPattern) -> Self { + let to_mkl_int_vec = |indices: &[usize]| { + indices + .iter() + .map(|idx| MKL_INT::try_from(*idx).expect("TODO: Handle properly if indices are too large for MKL_INT")) + .collect::>() + }; + + Self { + row_offsets: to_mkl_int_vec(pattern.major_offsets()), + column_indices: to_mkl_int_vec(pattern.minor_indices()), + rows: pattern.major_dim(), + cols: pattern.minor_dim(), + } + } + + pub fn as_mkl_csr<'a>(&'a self, values: &'a [f64]) -> CsrMatrixHandle<'a, f64> { + assert_eq!(self.column_indices.len(), values.len()); + CsrMatrixHandle::from_csr_data( + self.rows, + self.cols, + &self.row_offsets[..self.rows], + &self.row_offsets[1..], + &self.column_indices, + values, + ) + .expect("Sparse matrix construction should never fail") + } +} + +impl<'a> DifferentiableDynamicSystem for NewtonCgDynamicSystem<'a> { + fn set_state(&mut self, t: f64, u: DVectorSlice, v: DVectorSlice) -> Result<(), Box> { + self.alpha = None; + self.beta = None; + self.t = t; + self.solve_jacobian_combination_outdated = true; + self.stiffness_matrix_outdated = true; + self.workspace.u.copy_from(&u); + self.workspace.v.copy_from(&v); + apply_dirichlet_bcs_unknowns(t, &mut self.workspace.u, &mut self.workspace.v, self.bc); + Ok(()) + } + + fn init_solve_jacobian_combination(&mut self, alpha: Option, beta: Option) -> Result<(), Box> { + self.alpha = alpha; + self.beta = beta; + self.solve_jacobian_combination_outdated = true; + Ok(()) + } + + fn solve_jacobian_combination( + &mut self, + mut sol: DVectorSliceMut, + rhs: DVectorSlice, + ) -> Result<(), Box> { + profile!("solve jacobian combination"); + let matrices = &mut self.model_matrix_storage; + + if self.solve_jacobian_combination_outdated { + if self.stiffness_matrix_outdated { + let projected = false; + compute_stiffness_matrix_into( + self.model, + self.t, + DVectorSlice::from(&self.workspace.u), + DVectorSlice::from(&self.workspace.v), + &mut matrices.stiffness_matrix, + self.bc, + self.material_model, + projected, + ); + self.stiffness_matrix_outdated = false; + } + let damping_matrix = if matrices.has_damping_matrix { + Some(&matrices.damping_matrix) + } else { + None + }; + compute_jacobian_combination_into::( + &mut matrices.linear_combination_solve, + &matrices.stiffness_matrix, + damping_matrix, + &matrices.mass_matrix, + self.alpha, + self.beta, + Some(1.0), + self.bc, + ); + + { + profile!("update preconditioner"); + self.system_preconditioner + .update(&matrices.linear_combination_solve)?; + } + self.solve_jacobian_combination_outdated = false; + } + + { + profile!("conjugate gradient"); + let system_matrix = &matrices.linear_combination_solve; + + let mkl_connectivity = MklCsrConnectivity::from_csr_pattern(&system_matrix.sparsity_pattern()); + let mkl_system_matrix = mkl_connectivity.as_mkl_csr(system_matrix.values()); + + // TODO: Move this into the linear operator construction? + { + profile!("MKL sparse analysis"); + mkl_system_matrix + .set_mv_hint(SparseOperation::NonTranspose, &MatrixDescription::default(), 50) + .map_err(|_| Box::::from("MKL error during set_mv_hint"))?; + mkl_system_matrix + .optimize() + .map_err(|_| Box::::from("MKL error during set_mv_hint"))?; + } + + let mkl_linear_operator = MklCsrLinearOperator { + csr: &mkl_system_matrix, + }; + + // TODO: Use block diag preconditioner instead? From some quick tests + // it actually seems to perform worse than the diagonal preconditioner though + // let block_diag_preconditioner = BlockDiagonalPreconditioner3d::from_csr(&system_matrix)?; + + // TODO: Consider using a different initial guess than zero? + sol.fill(0.0); + + // TODO: Would be nice to have an adaptive criterion which would be scale-invariant, + // see e.g. Walker and Eisenstat's work on inexact Newton methods, + // or better yet we would use a criterion like that proposed by Nash, + // but then we'd need to additionally know the scalar value of the + // scalar potential implicitly being optimal + let residual_eps = 1e-5; + + let result = { + profile!("cg solve"); + ConjugateGradient::with_workspace(&mut self.workspace.cg_workspace) + .with_operator(mkl_linear_operator) + // TODO: Use a more effective diagonal preconditioner + .with_preconditioner(self.system_preconditioner.as_linear_operator()) + // .with_preconditioner(&block_diag_preconditioner) + // TODO: Use different stopping criterion? + .with_stopping_criterion(RelativeResidualCriterion::new(residual_eps)) + .with_max_iter(500) + .solve_with_guess(&rhs, &mut sol) + }; + + use fenris::cg::SolveErrorKind::*; + let output = match result { + Ok(output) => output, + Err(error) => { + match error.kind { + MaxIterationsReached { max_iter } => { + // We don't abort if we reach maximum number of iterations + // (Newton might still converge), but we make sure to log it. + warn!("CG reached maximum number of iterations ({}).", max_iter); + } + IndefiniteOperator => { + // We can still use the step produced by CG up to this point, + // because it is still ensured to be a descent direction. + warn!("CG encountered indefinite operator."); + } + // TODO: We don't expect preconditioner to be indefinite at the moment, + // so we treat that as an actual error + _ => return Err(Box::new(error)), + } + error.output + } + }; + + info!("CG iterations: {}", output.num_iterations); + } + Ok(()) + } + + fn apply_jacobian_combination( + &mut self, + _y: DVectorSliceMut, + _x: DVectorSlice, + ) -> Result<(), Box> { + unimplemented!() + } + + fn init_apply_jacobian_combination( + &mut self, + _alpha: Option, + _beta: Option, + _gamma: Option, + ) -> Result<(), Box> { + unimplemented!() + } +} + +// TODO: Move these to separate folder? +#[cfg(test)] +mod tests { + use super::*; + use fenris::nalgebra::DMatrix; + #[test] + fn build_block_preconditioner() { + let matrix = DMatrix::from_row_slice( + 9, + 9, + &[ + 26.0, 4.0, 12.0, -5.0, -7.0, 1.0, 2.0, 14.0, 11.0, 4.0, 23.0, 0.0, 2.0, -15.0, -11.0, 22.0, 0.0, 5.0, + 12.0, 0.0, 37.0, 6.0, -10.0, -13.0, 6.0, -4.0, 5.0, -5.0, 2.0, 6.0, 36.0, -8.0, 2.0, 5.0, -1.0, 6.0, + -7.0, -15.0, -10.0, -8.0, 24.0, -4.0, -19.0, -2.0, -13.0, 1.0, -11.0, -13.0, 2.0, -4.0, 31.0, -10.0, + 7.0, 4.0, 2.0, 22.0, 6.0, 5.0, -19.0, -10.0, 33.0, -7.0, 0.0, 14.0, 0.0, -4.0, -1.0, -2.0, 7.0, -7.0, + 37.0, 9.0, 11.0, 5.0, 5.0, 6.0, -13.0, 4.0, 0.0, 9.0, 26.0, + ], + ); + + let csr = CsrMatrix::from(&matrix); + let preconditioner = BlockDiagonalPreconditioner3d::from_csr(&csr).unwrap(); + + let expected_block1 = Matrix3::new(26.0, 4.0, 12.0, 4.0, 23.0, 0.0, 12.0, 0.0, 37.0) + .try_inverse() + .unwrap(); + let expected_block2 = Matrix3::new(36.0, -8.0, 2.0, -8.0, 24.0, -4.0, 2.0, -4.0, 31.0) + .try_inverse() + .unwrap(); + let expected_block3 = Matrix3::new(33.0, -7.0, 0.0, -7.0, 37.0, 9.0, 0.0, 9.0, 26.0) + .try_inverse() + .unwrap(); + + assert!((preconditioner.blocks[0] - expected_block1).norm() < 1e-13); + assert!((preconditioner.blocks[1] - expected_block2).norm() < 1e-13); + assert!((preconditioner.blocks[2] - expected_block3).norm() < 1e-13); + + let x = DVector::from_column_slice(&[8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0]); + let mut y = DVector::from_column_slice(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]); + preconditioner + .apply(DVectorSliceMut::from(&mut y), DVectorSlice::from(&x)) + .unwrap(); + + let mut y_expected = DVector::zeros(9); + y_expected + .index_mut((0..3, 0)) + .copy_from(&(&expected_block1 * x.index((0..3, 0)))); + y_expected + .index_mut((3..6, 0)) + .copy_from(&(&expected_block2 * x.index((3..6, 0)))); + y_expected + .index_mut((6..9, 0)) + .copy_from(&(&expected_block3 * x.index((6..9, 0)))); + + assert!((y - y_expected).norm() < 1e-13); + } +} diff --git a/simulation_toolbox/src/fem/schwarz_precond.rs b/simulation_toolbox/src/fem/schwarz_precond.rs new file mode 100644 index 0000000..e4c22d1 --- /dev/null +++ b/simulation_toolbox/src/fem/schwarz_precond.rs @@ -0,0 +1,372 @@ +use fenris::assembly::{CsrParAssembler, ElementAssembler, ElementConnectivityAssembler}; +use fenris::connectivity::{CellConnectivity, Connectivity}; +use fenris::nalgebra::{DMatrix, DMatrixSliceMut, DefaultAllocator, U3}; +use fenris::sparse::SparsityPattern; +use fenris::{CooMatrix, CsrMatrix}; +use std::error::Error; +use std::iter::once; +use std::ops::Add; +use std::sync::Arc; + +use coarse_prof::profile; +use fenris::allocators::ElementConnectivityAllocator; +use fenris::element::{ElementConnectivity, FiniteElement}; +use fenris::embedding::EmbeddedModel3d; +use fenris::geometry::ConvexPolyhedron; +use fenris::nested_vec::NestedVec; +use fenris::quadrature::Quadrature; +use fenris::space::FiniteElementSpace; +use hamilton::storages::VecStorage; +use hamilton::Component; +use log::info; +use paradis::coloring::sequential_greedy_coloring; +use paradis::DisjointSubsets; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct SchwarzPreconditionerComponent { + // TODO: Better names + /// Node-connectivities for elements that will be treated with the preconditioner + pub schwarz_connectivity: NestedVec, + /// Sorted list of indices of nodes that are not a member of preconditioned + /// elements. These nodes will be treated with a (possibly block-) Jacobi-type + /// preconditioner. + pub untreated_nodes: Vec, + + #[serde(skip)] + pub preconditioner: Option>, + + /// Colors must match schwarz_connectivity + #[serde(skip)] + pub colors: Option>, +} + +impl SchwarzPreconditionerComponent { + pub fn from_space(space: &S) -> Self + where + S: FiniteElementSpace, + DefaultAllocator: ElementConnectivityAllocator, + { + let mut schwarz_connectivity = NestedVec::new(); + for i in 0..space.num_connectivities() { + let conn = space.get_connectivity(i).unwrap(); + schwarz_connectivity.push(conn.vertex_indices()); + } + let untreated_nodes = Vec::new(); + let colors = sequential_greedy_coloring(&schwarz_connectivity); + Self { + schwarz_connectivity, + untreated_nodes, + preconditioner: None, + colors: Some(colors), + } + } + + pub fn from_embedded_model(model: &EmbeddedModel3d, volume_fraction_threshold: f64) -> Self + where + C: Connectivity + CellConnectivity, + C::Cell: for<'a> ConvexPolyhedron<'a, f64>, + C: ElementConnectivity, + DefaultAllocator: ElementConnectivityAllocator, + { + let mut schwarz_connectivity = NestedVec::new(); + for (index, interface_conn) in model.interface_connectivity().iter().enumerate() { + let bg_cell_volume = interface_conn + .cell(model.vertices()) + .unwrap() + .compute_volume(); + // TODO: Don't rely on quadrature for volume? Currently we use the + // mass quadrature because it's likely to be highest order, + // however since basically any correct quadrature should have the sum of the weights + // be equal to the volume, we could also use something else + let quadrature = &model + .mass_quadrature() + .expect( + "TODO: Handle missing mass quadrature, or better yet use a different \ + way to obtain embedded volumes", + ) + .interface_quadratures()[index]; + + let element = interface_conn.element(model.vertices()).unwrap(); + let embedded_volume = quadrature.integrate(|xi| element.reference_jacobian(xi).determinant()); + + let volume_fraction = embedded_volume / bg_cell_volume; + if volume_fraction <= volume_fraction_threshold { + schwarz_connectivity.push(interface_conn.vertex_indices()); + } + } + + // Tag nodes that belong to interface connectivity + let mut is_interface_node = vec![false; model.vertices().len()]; + for node_idx in schwarz_connectivity.iter_array_elements() { + is_interface_node[*node_idx] = true; + } + + let untreated_nodes: Vec = is_interface_node + .iter() + .enumerate() + .filter_map(|(idx, &is_interface)| if is_interface { None } else { Some(idx) }) + .collect(); + + let total_num_elements = model.background_mesh().connectivity().len(); + let proportion = schwarz_connectivity.len() as f64 / total_num_elements as f64; + info!( + "Preconditioning {} out of {} elements ({:2.1} %) with Schwarz preconditioner.", + schwarz_connectivity.len(), + total_num_elements, + 100.0 * proportion + ); + + let colors = sequential_greedy_coloring(&schwarz_connectivity); + Self { + schwarz_connectivity, + untreated_nodes, + preconditioner: None, + colors: Some(colors), + } + } +} + +impl Component for SchwarzPreconditionerComponent { + type Storage = VecStorage; +} + +pub fn build_preconditioner_pattern( + num_nodes: usize, + schwarz_connectivity: &NestedVec, + untreated_nodes: &[usize], + solution_dim: usize, +) -> Arc { + // TODO: This can be done *much* more efficiently + let n = solution_dim * num_nodes; + let mut coo = CooMatrix::new(n, n); + + for node_indices in schwarz_connectivity.iter() { + for node_i in node_indices { + for node_j in node_indices { + for i in 0..solution_dim { + for j in 0..solution_dim { + coo.push(solution_dim * *node_i + i, solution_dim * *node_j + j, 0.0); + } + } + } + } + } + + // Add block diagonal entries for untreated nodes + for &node_idx in untreated_nodes { + for i in 0..solution_dim { + coo.push(solution_dim * node_idx + i, solution_dim * node_idx + i, 0.0); + // Below was for old block diagonal preconditioning + // for j in 0 .. solution_dim { + // coo.push(solution_dim * node_idx + i, solution_dim * node_idx + j, 0.0); + // } + } + } + + let csr = coo.to_csr(Add::add); + csr.sparsity_pattern().clone() +} + +pub fn build_schwarz_preconditioner_into( + preconditioner: &mut CsrMatrix, + system_matrix: &CsrMatrix, + schwarz_connectivity: &NestedVec, + untreated_nodes: &[usize], + colors: &[DisjointSubsets], + solution_dim: usize, +) -> Result<(), Box> { + let mut csr_assembler = CsrParAssembler::default(); + let element_assembler = SchwarzElementAssembler { + matrix: system_matrix, + schwarz_connectivity, + solution_dim, + }; + + { + profile!("assemble"); + csr_assembler + .assemble_into_csr(preconditioner, &colors, &element_assembler) + .map_err(|err| err as Box)?; + } + + // Add remaining diagonal entries + for &node_idx in untreated_nodes { + for i in 0..solution_dim { + let row_index = solution_dim * node_idx + i; + let mut precond_row = preconditioner.row_mut(row_index); + assert_eq!(precond_row.nnz(), 1); + assert_eq!(precond_row.col_at_local_index(0), row_index); + + let matrix_row = system_matrix.row(row_index); + let entry = matrix_row.get(row_index).unwrap_or(0.0); + + if entry != 0.0 { + precond_row.values_mut()[0] = entry.recip(); + } else { + return Err(Box::::from( + "Can not construct Schwarz preconditioner: \ + Diagonal entry is zero", + )); + } + } + } + + Ok(()) +} + +pub fn build_schwarz_preconditioner( + matrix: &CsrMatrix, + schwarz_connectivity: &NestedVec, + untreated_nodes: &[usize], + solution_dim: usize, +) -> Result, Box> { + profile!("schwarz preconditioner construction"); + assert_eq!(matrix.nrows(), matrix.ncols()); + assert_eq!(matrix.nrows() % solution_dim, 0); + let num_nodes = matrix.nrows() / solution_dim; + + let pattern = { + profile!("build pattern"); + build_preconditioner_pattern(num_nodes, schwarz_connectivity, untreated_nodes, solution_dim) + }; + + let nnz = pattern.nnz(); + let mut result = CsrMatrix::from_pattern_and_values(pattern, vec![0.0; nnz]); + + let colors = sequential_greedy_coloring(schwarz_connectivity); + + build_schwarz_preconditioner_into( + &mut result, + matrix, + schwarz_connectivity, + untreated_nodes, + &colors, + solution_dim, + )?; + Ok(result) +} + +struct SchwarzElementAssembler<'a> { + matrix: &'a CsrMatrix, + solution_dim: usize, + schwarz_connectivity: &'a NestedVec, +} + +impl<'a> ElementConnectivityAssembler for SchwarzElementAssembler<'a> { + fn num_nodes(&self) -> usize { + assert_eq!(self.matrix.nrows() % self.solution_dim, 0); + self.matrix.nrows() / self.solution_dim() + } + + fn solution_dim(&self) -> usize { + self.solution_dim + } + + fn num_elements(&self) -> usize { + self.schwarz_connectivity.len() + } + + fn element_node_count(&self, element_index: usize) -> usize { + self.schwarz_connectivity + .get(element_index) + .expect("Element index is assumed to be in bounds") + .len() + } + + fn populate_element_nodes(&self, output: &mut [usize], element_index: usize) { + let indices = self + .schwarz_connectivity + .get(element_index) + .expect("Element index is assumed to be in bounds"); + assert_eq!(output.len(), indices.len()); + output.copy_from_slice(indices); + } +} + +impl<'a> ElementAssembler for SchwarzElementAssembler<'a> { + fn assemble_element_matrix_into( + &self, + mut output: DMatrixSliceMut, + element_index: usize, + ) -> Result<(), Box> { + let node_indices = self + .schwarz_connectivity + .get(element_index) + .expect("Element index is assumed to be in bounds"); + let sdim = self.solution_dim; + + let element_matrix_dim = sdim * node_indices.len(); + + let mut element_block = DMatrix::zeros(element_matrix_dim, element_matrix_dim); + + // TODO: Write this is as a "gather" routine in CSR? + for (local_node_i, global_node_i) in node_indices.iter().enumerate() { + for i in 0..sdim { + let local_row = sdim * local_node_i + i; + let global_row = sdim * *global_node_i + i; + + let csr_row = self.matrix.row(global_row); + let mut element_block_row = element_block.row_mut(local_row); + + for (local_node_j, global_node_j) in node_indices.iter().enumerate() { + for j in 0..sdim { + let local_col = sdim * local_node_j + j; + let global_col = sdim * *global_node_j + j; + element_block_row[local_col] = csr_row.get(global_col).expect("TODO: Error handling?"); + } + } + } + } + + let element_block_inverse = invert_element_block(element_block); + output.copy_from(&element_block_inverse); + + Ok(()) + } +} + +fn invert_element_block(element_block_matrix: DMatrix) -> DMatrix { + let mut block_eigen = nalgebra_lapack::SymmetricEigen::new(element_block_matrix); + let max_eval = block_eigen.eigenvalues.amax(); + let lower_threshold = 1e-12 * max_eval; + + for eval in &mut block_eigen.eigenvalues { + *eval = + // Note: We flip the sign of negative eigenvalues so as to produce + // a positive definite preconditioner. + if eval.abs() >= lower_threshold { 1.0 / eval.abs() } + else { 1.0 / (lower_threshold) }; + } + block_eigen.recompose() +} + +pub fn element_wise_dense_additive_schwarz_preconditioner(matrix: &DMatrix, connectivity: &[C]) -> DMatrix +where + C: Connectivity, +{ + let mut result = DMatrix::zeros(matrix.nrows(), matrix.ncols()); + for conn in connectivity { + // Gather the element "patch matrix" + let indices: Vec<_> = conn + .vertex_indices() + .iter() + // TODO: I hate this: Fix? + .flat_map(|v_idx| { + once(3 * v_idx) + .chain(once(3 * v_idx + 1)) + .chain(once(3 * v_idx + 2)) + }) + .collect(); + assert_eq!(indices.len(), 3 * conn.vertex_indices().len()); + let patch_matrix = matrix.select_rows(&indices).select_columns(&indices); + let patch_inverse = invert_element_block(patch_matrix); + + for (c_local, &c_global) in indices.iter().enumerate() { + for (r_local, &r_global) in indices.iter().enumerate() { + result[(r_global, c_global)] += patch_inverse[(r_local, c_local)]; + } + } + } + result +} diff --git a/simulation_toolbox/src/fem/system_assembly.rs b/simulation_toolbox/src/fem/system_assembly.rs new file mode 100644 index 0000000..7d5456f --- /dev/null +++ b/simulation_toolbox/src/fem/system_assembly.rs @@ -0,0 +1,235 @@ +use std::convert::identity; +use std::error::Error; + +use coarse_prof::profile; +use fenris::assembly::{ + apply_homogeneous_dirichlet_bc_csr, DefaultSemidefiniteProjection, ElementMatrixTransformation, NoTransformation, +}; +use fenris::nalgebra::allocator::Allocator; +use fenris::nalgebra::{DMatrixSliceMut, DVector, DVectorSlice, DVectorSliceMut, DefaultAllocator, DimName}; +use fenris::solid::ElasticityModelParallel; +use fenris::{solid, CsrMatrix}; +use nalgebra_lapack::SymmetricEigen; + +use crate::components::Gravity; +use crate::fem::{DirichletBoundaryConditions, OptionalDirichletBoundaryConditions}; + +pub struct LapackSemidefiniteProjection; + +impl ElementMatrixTransformation for LapackSemidefiniteProjection { + fn transform_element_matrix(&self, element_matrix: &mut DMatrixSliceMut) { + let mut eigen_decomposition = SymmetricEigen::new(element_matrix.clone_owned()); + for eval in &mut eigen_decomposition.eigenvalues { + *eval = f64::max(0.0, *eval); + } + // TODO: Don't recompose if we don't need to make changes + element_matrix.copy_from(&eigen_decomposition.recompose()); + } +} + +pub fn compute_gravity_density( + model: &dyn ElasticityModelParallel, + gravity: &Gravity, +) -> Result, Box> +where + D: DimName, + DefaultAllocator: Allocator + Allocator, +{ + let ndof = model.ndof(); + let mut g_force_density = DVector::zeros(ndof); + match gravity { + Gravity::Scalar(g) => { + for i in 0..ndof { + if (i % D::dim()) + 1 == D::dim() { + g_force_density[i] = *g; + } + } + } + Gravity::Vec2(g_2d) => { + if D::dim() != 2 { + return Err(Box::from( + "Trying to apply 2d gravity to model that is not two dimensional", + )); + } + + for i in 0..ndof { + let j = i % 2; + g_force_density[i] = g_2d[j]; + } + } + Gravity::Vec3(g_3d) => { + if D::dim() != 3 { + return Err(Box::from( + "Trying to apply 3d gravity to model that is not three dimensional", + )); + } + + for i in 0..ndof { + let j = i % 3; + g_force_density[i] = g_3d[j]; + } + } + }; + + Ok(g_force_density) +} + +/// Applies the system's Dirichlet boundary conditions to the vectors of unknowns +pub fn apply_dirichlet_bcs_unknowns<'a>( + t: f64, + u: impl Into>, + v: impl Into>, + dirichlet_bcs: Option<&dyn DirichletBoundaryConditions>, +) { + if let Some(dirichlet_bcs) = dirichlet_bcs { + let mut u = u.into(); + let mut v = v.into(); + let d = dirichlet_bcs.solution_dim(); + + let mut bcs_u = DVector::zeros(dirichlet_bcs.nrows()); + let mut bcs_v = DVector::zeros(dirichlet_bcs.nrows()); + + // Evaluate boundary conditions + dirichlet_bcs.apply_displacement_bcs(DVectorSliceMut::from(&mut bcs_u), t); + dirichlet_bcs.apply_velocity_bcs(DVectorSliceMut::from(&mut bcs_v), t); + + // Apply boundary conditions + for (i_local, i_global) in dirichlet_bcs.nodes().iter().copied().enumerate() { + for id in 0..d { + *u.index_mut(d * i_global + id) = *bcs_u.index(d * i_local + id); + *v.index_mut(d * i_global + id) = *bcs_v.index(d * i_local + id); + } + } + } +} + +/// Note: Does not apply boundary conditions to the stiffness matrix itself +pub fn compute_stiffness_matrix_into( + model: &dyn ElasticityModelParallel, + t: f64, + u: DVectorSlice, + v: DVectorSlice, + stiffness_matrix: &mut CsrMatrix, + dirichlet_bcs: Option<&dyn DirichletBoundaryConditions>, + material_model: &(dyn Sync + solid::ElasticMaterialModel), + // Whether we should ensure that the returned matrix is semidefinite + projected: bool, +) where + DefaultAllocator: Allocator + Allocator, +{ + // TODO: Avoid all this unnecessary memory allocation + + // Apply boundary conditions + let mut u = u.clone_owned(); + let mut v = v.clone_owned(); + apply_dirichlet_bcs_unknowns(t, &mut u, &mut v, dirichlet_bcs); + + { + profile!("assemble stiffness"); + stiffness_matrix.fill_par(0.0); + + if !projected || material_model.is_positive_semi_definite() { + model.assemble_transformed_stiffness_into_par(stiffness_matrix, &u, material_model, &NoTransformation); + } else { + model.assemble_transformed_stiffness_into_par( + stiffness_matrix, + &u, + material_model, + &DefaultSemidefiniteProjection, + ); + } + } +} + +/// Note: Does not apply boundary conditions to the damping matrix itself +pub fn compute_damping_matrix_into( + model: &dyn ElasticityModelParallel, + u: DVectorSlice, + stiffness_matrix: &mut CsrMatrix, + damping_matrix: &mut CsrMatrix, + mass_matrix: &CsrMatrix, + mass_damping_coefficient: Option, + stiffness_damping_coefficient: Option, + material_model: &(dyn Sync + solid::ElasticMaterialModel), +) -> bool +where + D: DimName, + DefaultAllocator: Allocator + Allocator, +{ + profile!("assemble damping matrix"); + + if mass_damping_coefficient.is_none() && stiffness_damping_coefficient.is_none() { + return false; + } + + if stiffness_damping_coefficient.is_some() { + compute_stiffness_matrix_into( + model, + 0.0, + DVectorSlice::from(u), + DVectorSlice::from(u), + stiffness_matrix, + None, + material_model, + // Always use projection to ensure that the damping matrix is + // at least positive semidefinite + true, + ); + } + + // Compute damping matrix as `D = gamma * M + alpha * K` + compute_jacobian_combination_into( + damping_matrix, + stiffness_matrix, + None, + mass_matrix, + stiffness_damping_coefficient.map(|alpha| -alpha), + None, + mass_damping_coefficient, + None, + ); + + return true; +} + +/// Computes a Jacobian linear combination `J = gamma * M - beta * D - alpha * K` and applies boundary conditions. +pub fn compute_jacobian_combination_into<'a, D: DimName>( + jacobian_combination: &mut CsrMatrix, + stiffness_matrix: &CsrMatrix, + damping_matrix: Option<&CsrMatrix>, + mass_matrix: &CsrMatrix, + alpha: Option, + beta: Option, + gamma: Option, + dirichlet_bcs: Option<&dyn DirichletBoundaryConditions>, +) where + DefaultAllocator: Allocator + Allocator, +{ + { + profile!("compute jacobian combination"); + // J = gamma * M - beta * D - alpha * K + let coefficient_matrix_pairs = [ + // Stiffness matrix: negate alpha because df/dx = -K + alpha.map(|alpha| (-alpha, stiffness_matrix)), + // Damping matrix: negate beta because df/dv = -D + // Nested map, because there might not be a damping matrix + beta.and_then(|beta| damping_matrix.map(|damping_matrix| (-beta, damping_matrix))), + // Mass matrix + gamma.map(|gamma| (gamma, mass_matrix)), + ]; + + jacobian_combination.fill_par(0.0); + jacobian_combination.add_assign_linear_combination_par( + coefficient_matrix_pairs + .iter() + .cloned() + // Skip None entries + .filter_map(identity), + ); + } + + { + profile!("apply Dirichlet BC CSR"); + apply_homogeneous_dirichlet_bc_csr::<_, D>(jacobian_combination, &dirichlet_bcs.nodes()); + } +} diff --git a/simulation_toolbox/src/io/json_helper.rs b/simulation_toolbox/src/io/json_helper.rs new file mode 100644 index 0000000..0438079 --- /dev/null +++ b/simulation_toolbox/src/io/json_helper.rs @@ -0,0 +1,162 @@ +use std::borrow::Borrow; +use std::borrow::Cow; +use std::error::Error; +use std::fs; +use std::ops::Deref; +use std::path::Path; + +use fenris::nalgebra::{Translation3, Vector3}; +pub use serde_json; + +#[derive(Clone, Debug)] +pub struct JsonWrapper<'a> { + pub value: Cow<'a, serde_json::Value>, +} + +/// Convenience function to parse a file to a serde_json::Value +pub fn parse_json_from_path>(path: P) -> Result, Box> { + let json_string = fs::read_to_string(path.as_ref()).map_err(|e| { + format!( + "Unable to open JSON file '{}' for reading ({:?})", + path.as_ref().to_string_lossy(), + e + ) + })?; + + let json_value = serde_json::de::from_str(&json_string).map_err(|e| { + format!( + "Error during parsing of JSON file '{}': {}", + path.as_ref().to_string_lossy(), + e + ) + })?; + + Ok(JsonWrapper { value: json_value }) +} + +/// Convenience trait to deserialize JSON values into types +pub trait TryFromJson: Sized { + fn try_from_json>(json: J) -> Result>; +} + +impl<'a> JsonWrapper<'a> { + pub fn new(value: serde_json::Value) -> Self { + Self { + value: Cow::Owned(value), + } + } + + pub fn new_borrowed(value: &'a serde_json::Value) -> Self { + Self { + value: Cow::Borrowed(value), + } + } + + pub fn to_string_pretty(&self) -> Result> { + Ok(serde_json::to_string_pretty(self.as_value())?) + } + + /// Return a reference to the contained serde_json::Value + pub fn as_value(&self) -> &serde_json::Value { + &self.value + } + + pub fn members(&self) -> Result, Box> { + Ok(self + .as_value() + .as_array() + .ok_or_else(|| Box::::from("Expected array"))? + .iter() + .map(|v| JsonWrapper::new_borrowed(v))) + } + + pub fn get_json_value_ref>(&self, index: S) -> Result<&serde_json::Value, Box> { + if let Some(obj) = self.as_object() { + return obj + .get(index.as_ref()) + .ok_or_else(|| Box::from(format!("Entry \"{}\" not found in JSON file", index.as_ref()))); + } else { + return Err(Box::from(format!( + "Cannot access entry \"{}\" as parent JSON value is not an object", + index.as_ref() + ))); + } + } + + pub fn get>(&self, index: S) -> Result> { + self.get_json_value_ref(index) + .map(|v| JsonWrapper::new_borrowed(v)) + } + + pub fn get_bool>(&self, index: S) -> Result> { + self.get_json_value_ref(index.as_ref()) + .map(|v| v.as_bool())? + .ok_or_else(|| { + Box::from(format!( + "The entry \"{}\" does not contain a bool value", + index.as_ref() + )) + }) + } + + pub fn get_f64>(&self, index: S) -> Result> { + self.get_json_value_ref(index.as_ref()) + .map(|v| v.as_f64())? + .ok_or_else(|| Box::from(format!("The entry \"{}\" does not contain a f64 value", index.as_ref()))) + } + + pub fn get_i64>(&self, index: S) -> Result> { + self.get_json_value_ref(index.as_ref()) + .map(|v| v.as_i64())? + .ok_or_else(|| Box::from(format!("The entry \"{}\" does not contain a i64 value", index.as_ref()))) + } +} + +impl<'a> Borrow for JsonWrapper<'a> { + fn borrow(&self) -> &serde_json::Value { + &self.as_value() + } +} + +impl<'a> Deref for JsonWrapper<'a> { + type Target = serde_json::Value; + + fn deref(&self) -> &Self::Target { + &self.as_value() + } +} + +impl TryFromJson for Vector3 { + fn try_from_json>(json: J) -> Result> { + Ok(Vector3::from(<[f64; 3] as TryFromJson>::try_from_json(json)?)) + } +} + +impl TryFromJson for Translation3 { + fn try_from_json>(json: J) -> Result> { + Ok(Translation3::from(Vector3::try_from_json(json)?)) + } +} + +// TODO: Can this be implemented using Serde? Otherwise use macro? +impl TryFromJson for [f64; 3] { + fn try_from_json>(json: J) -> Result> { + if let serde_json::Value::Array(arr) = json.borrow() { + Ok([ + arr[0] + .as_f64() + .ok_or_else(|| "Expected float component of a three component array")?, + arr[1] + .as_f64() + .ok_or_else(|| "Expected float component of a three component array")?, + arr[2] + .as_f64() + .ok_or_else(|| "Expected float component of a three component array")?, + ]) + } else { + Err(Box::from( + "Expected a three component float array in JSON, but no array object found", + )) + } + } +} diff --git a/simulation_toolbox/src/io/mod.rs b/simulation_toolbox/src/io/mod.rs new file mode 100644 index 0000000..8967266 --- /dev/null +++ b/simulation_toolbox/src/io/mod.rs @@ -0,0 +1,5 @@ +pub mod json_helper; +pub mod msh; +pub mod obj; +pub mod ply; +pub mod vtk; diff --git a/simulation_toolbox/src/io/msh.rs b/simulation_toolbox/src/io/msh.rs new file mode 100644 index 0000000..b338175 --- /dev/null +++ b/simulation_toolbox/src/io/msh.rs @@ -0,0 +1,260 @@ +use fenris::connectivity::{Connectivity, Tet4Connectivity, Tri3d2Connectivity, Tri3d3Connectivity}; +use fenris::mesh::Mesh; +use fenris::nalgebra::{allocator::Allocator, DefaultAllocator, DimName, Point, Point2, Point3, RealField, U2, U3}; +use log::warn; +use mshio::mshfile::{ElementType, MshFile}; +use std::convert::TryInto; +use std::error::Error; + +pub trait TryFromMshNodeBlock +where + Self: Sized, + T: RealField, + D: DimName, + F: mshio::MshFloatT, + I: mshio::MshIntT, + Point: TryVertexFromMshNode, + DefaultAllocator: Allocator, +{ + fn try_from_node_block(node_block: &mshio::NodeBlock) -> Result>, Box> { + // Ensure that node tags are consecutive + if node_block.node_tags.is_some() { + return Err(Box::from("Node block tags are not consecutive")); + } else { + if node_block + .entity_dim + .to_usize() + .ok_or_else(|| "Error converting node block entity dimension to usize")? + != D::dim() + { + warn!("Warning: Node block entity does not have the right dimension for this mesh. Will be read as if they were of the same dimension."); + /* + return Err(Box::from( + "Node block entity does not have the right dimension for this mesh", + )); + */ + } + + let mut vertices = Vec::with_capacity(node_block.nodes.len()); + + // Convert MSH vertices to points + for node in &node_block.nodes { + vertices.push(Point::try_vertex_from_msh_node(node)?); + } + + Ok(vertices) + } + } +} + +impl TryFromMshNodeBlock for Point +where + T: RealField, + D: DimName, + F: mshio::MshFloatT, + I: mshio::MshIntT, + Point: TryVertexFromMshNode, + DefaultAllocator: Allocator, +{ +} + +pub trait TryVertexFromMshNode +where + Self: Sized, + T: RealField, + D: DimName, + F: mshio::MshFloatT, + DefaultAllocator: Allocator, +{ + fn try_vertex_from_msh_node(node: &mshio::Node) -> Result, Box>; +} + +macro_rules! f_to_t { + ($component:expr) => { + T::from_f64( + $component + .to_f64() + .ok_or_else(|| "Error converting node coordinate to f64")?, + ) + .ok_or_else(|| "Error converting node coordinate to mesh data type")? + }; +} + +impl TryVertexFromMshNode for Point2 +where + T: RealField, + F: mshio::MshFloatT, +{ + fn try_vertex_from_msh_node(node: &mshio::Node) -> Result> { + // TODO: Ensure that node.z is zero? + Ok(Self::new(f_to_t!(node.x), f_to_t!(node.y))) + } +} + +impl TryVertexFromMshNode for Point3 +where + T: RealField, + F: mshio::MshFloatT, +{ + fn try_vertex_from_msh_node(node: &mshio::Node) -> Result> { + Ok(Self::new(f_to_t!(node.x), f_to_t!(node.y), f_to_t!(node.z))) + } +} + +pub trait TryFromMshElementBlock +where + Self: Sized, + C: Connectivity + TryConnectivityFromMshElement, + I: mshio::MshIntT, +{ + fn try_from_element_block(element_block: &mshio::ElementBlock) -> Result, Box> { + let requested_msh_element_type = C::msh_element_type(); + if element_block.element_type != requested_msh_element_type { + warn!("Warning: Detected connectivity in the MSH file that does not match the requested connectivity. It will be ignored."); + return Ok(Vec::new()); + /* + return Err(Box::from( + "Connectivity in the MSH file does not match the requested connectivity.", + )); + */ + } else { + let mut connectivity = Vec::with_capacity(element_block.elements.len()); + let requested_nodes = requested_msh_element_type + .nodes() + .map_err(|_| "Unimplemented element type requested")?; + + for element in &element_block.elements { + if element.nodes.len() < requested_nodes { + return Err(Box::from("Not enough nodes to initialize connectivity.")); + } + connectivity.push(C::try_connectivity_from_msh_element(element)?); + } + + Ok(connectivity) + } + } +} + +impl TryFromMshElementBlock for C +where + C: Connectivity + TryConnectivityFromMshElement, + I: mshio::MshIntT, +{ +} + +pub trait TryConnectivityFromMshElement +where + Self: Sized, + C: Connectivity, +{ + fn msh_element_type() -> ElementType; + + fn try_connectivity_from_msh_element(element: &mshio::Element) -> Result>; +} + +impl TryConnectivityFromMshElement for Tri3d2Connectivity { + fn msh_element_type() -> ElementType { + ElementType::Tri3 + } + + fn try_connectivity_from_msh_element(element: &mshio::Element) -> Result> { + Ok(Self([ + (element.nodes[0] - 1).try_into().unwrap(), + (element.nodes[1] - 1).try_into().unwrap(), + (element.nodes[2] - 1).try_into().unwrap(), + ])) + } +} + +impl TryConnectivityFromMshElement for Tri3d3Connectivity { + fn msh_element_type() -> ElementType { + ElementType::Tri3 + } + + fn try_connectivity_from_msh_element(element: &mshio::Element) -> Result> { + Ok(Self([ + (element.nodes[0] - 1).try_into().unwrap(), + (element.nodes[1] - 1).try_into().unwrap(), + (element.nodes[2] - 1).try_into().unwrap(), + ])) + } +} + +impl TryConnectivityFromMshElement for Tet4Connectivity { + fn msh_element_type() -> ElementType { + ElementType::Tet4 + } + + fn try_connectivity_from_msh_element(element: &mshio::Element) -> Result> { + Ok(Self([ + (element.nodes[0] - 1).try_into().unwrap(), + (element.nodes[1] - 1).try_into().unwrap(), + (element.nodes[2] - 1).try_into().unwrap(), + (element.nodes[3] - 1).try_into().unwrap(), + ])) + } +} + +/// Tries to create a Fenris mesh from bytes representing a MSH file +pub fn try_mesh_from_bytes(msh_bytes: &[u8]) -> Result, Box> +where + T: RealField, + D: DimName, + C: Connectivity + TryConnectivityFromMshElement, + Point: TryVertexFromMshNode, + DefaultAllocator: Allocator, +{ + let msh = match mshio::parse_msh_bytes(msh_bytes) { + Ok(msh) => msh, + Err(e) => return Err(Box::from(format!("Error during MSH parsing ({})", e))), + }; + + try_mesh_from_msh_file(msh) +} + +/// Creates a Mesh from a MshFile +pub fn try_mesh_from_msh_file(msh: MshFile) -> Result, Box> +where + T: RealField, + D: DimName, + C: Connectivity + TryConnectivityFromMshElement, + Point: TryVertexFromMshNode, + DefaultAllocator: Allocator, +{ + let mut msh = msh; + let msh_nodes = msh + .data + .nodes + .take() + .ok_or("MSH file does not contain nodes")?; + let msh_elements = msh + .data + .elements + .take() + .ok_or("MSH file does not contain elements")?; + + let mut vertices = Vec::new(); + let mut connectivity = Vec::new(); + + for node_block in &msh_nodes.node_blocks { + // Ensure that node tags are consecutive + if node_block.node_tags.is_some() { + return Err(Box::from("Node block tags are not consecutive")); + } + + let block_vertices = Point::try_from_node_block(node_block)?; + vertices.extend(block_vertices); + } + + for element_block in &msh_elements.element_blocks { + // Ensure that element tags are consecutive + if element_block.element_tags.is_some() { + return Err(Box::from("Element block tags are not consecutive")); + } + + let block_connectivity = C::try_from_element_block(element_block)?; + connectivity.extend(block_connectivity); + } + + Ok(Mesh::from_vertices_and_connectivity(vertices, connectivity)) +} diff --git a/simulation_toolbox/src/io/obj.rs b/simulation_toolbox/src/io/obj.rs new file mode 100644 index 0000000..501a7b4 --- /dev/null +++ b/simulation_toolbox/src/io/obj.rs @@ -0,0 +1,39 @@ +use fenris::geometry::polymesh::PolyMesh3d; +use fenris::nalgebra::Point3; +use fenris::nested_vec::NestedVec; +use obj::{IndexTuple, Obj, SimplePolygon}; +use std::error::Error; +use std::path::Path; + +pub fn load_single_surface_polymesh3d_obj(path: impl AsRef) -> Result, Box> { + load_single_surface_polymesh3d_obj_(path.as_ref()) +} + +fn load_single_surface_polymesh3d_obj_(path: &Path) -> Result, Box> { + let obj_file = Obj::load(path)?; + + let vertices: Vec<_> = obj_file + .data + .position + .iter() + .map(|v| [v[0] as f64, v[1] as f64, v[2] as f64]) + .map(Point3::from) + .collect(); + + if obj_file.data.objects.len() != 1 { + return Err(Box::from("Obj file must contain exactly one object")); + } + + let object = obj_file.data.objects.first().unwrap(); + let mut faces = NestedVec::new(); + for group in &object.groups { + for SimplePolygon(ref index_tuples) in &group.polys { + let mut appender = faces.begin_array(); + for IndexTuple(vertex_idx, _, _) in index_tuples { + appender.push_single(*vertex_idx); + } + } + } + + Ok(PolyMesh3d::from_poly_data(vertices, faces, NestedVec::new())) +} diff --git a/simulation_toolbox/src/io/ply.rs b/simulation_toolbox/src/io/ply.rs new file mode 100644 index 0000000..84792da --- /dev/null +++ b/simulation_toolbox/src/io/ply.rs @@ -0,0 +1,615 @@ +use std::error::Error; +use std::fmt; +use std::fmt::Write as FmtWrite; +use std::fs::{create_dir_all, File}; +use std::io; +use std::io::Write as IoWrite; +use std::path::{Path, PathBuf}; + +use fenris::connectivity::{Connectivity, Quad4d2Connectivity, Tri3d2Connectivity}; +use fenris::geometry::polymesh::{PolyMesh, PolyMesh3d}; +use fenris::mesh::{Mesh, Mesh2d, TriangleMesh2d}; +use fenris::nalgebra::allocator::Allocator; +use fenris::nalgebra::{DefaultAllocator, DimName, Point2, RealField, Scalar, U2, U3}; +use fenris::nested_vec::NestedVec; +use hamilton::{StorageContainer, System}; +use itertools::Itertools; +use num::ToPrimitive; + +use crate::components::{ + get_export_sequence_index, Name, PointInterpolator, PolyMesh2dCollection, PolyMesh3dCollection, VolumeMesh2d, +}; +use crate::fem::{FiniteElementElasticModel2d, FiniteElementElasticModel3d, FiniteElementModel2d}; + +/// A system that writes named 2D volume meshes to PLY files. +#[derive(Debug)] +pub struct PlyVolumeMesh2dOutput { + pub base_path: PathBuf, +} + +/// A system that writes named 2D FEM meshes to PLY files. +#[derive(Debug)] +pub struct PlyFem2dOutput { + pub base_path: PathBuf, +} + +/// A system that writes named from PointInterpolator components to PLY files (as degenerate triangle soups). +#[derive(Debug)] +pub struct PlyInterpolatedPoints2dOutput { + pub base_path: PathBuf, +} + +/* +/// A system that writes named 3D FEM meshes to PLY files. +#[derive(Debug)] +pub struct PlyFem3dOutput { + pub base_path: PathBuf, +} +*/ + +/// A system that writes the faces of named PolyMesh2D meshes to PLY files. +#[derive(Debug)] +pub struct PlyPolyMesh2dOutput { + pub base_path: PathBuf, +} + +/// A system that writes the faces of named Polymesh3D meshes to PLY files. +#[derive(Debug)] +pub struct PlyPolyMesh3dOutput { + pub base_path: PathBuf, +} + +impl fmt::Display for PlyVolumeMesh2dOutput { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "PlyVolumeMesh2dOutput") + } +} + +impl fmt::Display for PlyFem2dOutput { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "PlyFem2dOutput") + } +} + +impl fmt::Display for PlyInterpolatedPoints2dOutput { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "PlyInterpolatedPoints2dOutput") + } +} + +/* +impl fmt::Display for PlyFem3dOutput { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "PlyFem3dOutput") + } +} +*/ + +impl fmt::Display for PlyPolyMesh2dOutput { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "PlyPolyMesh2dOutput") + } +} + +impl fmt::Display for PlyPolyMesh3dOutput { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "PlyPolyMesh3dOutput") + } +} + +/// Writes the faces stored in a polymesh to a Writer in PLY ASCII format +fn write_polymesh_faces_to_ply_ascii(writer: &mut W, polymesh: &PolyMesh) -> Result<(), Box> +where + W: io::Write, + T: Scalar + ToPrimitive, + D: DimName, + DefaultAllocator: Allocator, +{ + write!( + writer, + // TODO: Re-enable export of normals and vertex colors once we properly + // support this in e.g. PolyMesh3d. The old format is commented out below + "\ +ply +format ascii 1.0 +element vertex {} +property float x +property float y +property float z +element face {} +property list uchar int vertex_index +end_header +", + // "\ + // ply + // format ascii 1.0 + // element vertex {} + // property float x + // property float y + // property float z + // property float nx + // property float ny + // property float nz + // property uchar red + // property uchar green + // property uchar blue + // element face {} + // property list uchar int vertex_index + // end_header + // ", + polymesh.vertices().len(), + polymesh.num_faces() + )?; + + // Vertices + match D::dim() { + 1 => { + for v in polymesh.vertices() { + let coords = &v.coords; + write!( + writer, + "{} 0.0 0.0\n", + // "{} 0.0 0.0 0 0 1 150 0 0\n", + coords[0].to_f64().unwrap() as f32 + )?; + } + } + 2 => { + for v in polymesh.vertices() { + let coords = &v.coords; + write!( + writer, + "{} {} 0.0\n", + // "{} {} 0.0 0 0 1 150 0 0\n", + coords[0].to_f64().unwrap() as f32, + coords[1].to_f64().unwrap() as f32 + )?; + } + } + 3 => { + for v in polymesh.vertices() { + let coords = &v.coords; + write!( + writer, + "{} {} {}\n", + // "{} {} {} 0 0 1 150 0 0\n", + coords[0].to_f64().unwrap() as f32, + coords[1].to_f64().unwrap() as f32, + coords[2].to_f64().unwrap() as f32 + )?; + } + } + _ => { + return Err(Box::from(format!( + "Cannot output vertices of dimension {} into PLY", + D::dim() + ))); + } + } + + // Faces + for face in polymesh.face_connectivity_iter() { + let indices = face.iter().map(|v| v.to_string()).join(" "); + write!(writer, "{} ", face.len())?; + write!(writer, "{}", indices)?; + write!(writer, "\n")?; + } + + Ok(()) +} + +pub fn dump_polymesh_faces_ply( + mesh: &PolyMesh3d, + base_path: impl AsRef, + filename: impl Into, +) -> Result<(), Box> { + let filename = filename.into(); + let ply_file_path = base_path.as_ref().join(&filename); + + // Create any necessary parent directories (otherwise creating file will fail) + if let Some(dir) = ply_file_path.parent() { + create_dir_all(dir)?; + } + + let file = File::create(ply_file_path)?; + let mut ply_writer = io::BufWriter::new(file); + write_polymesh_faces_to_ply_ascii(&mut ply_writer, mesh) +} + +/// Creates a file with the specified filename, appends sequence index, opens it +fn prepare_ply_file_for_writing, S: Into>( + data: &StorageContainer, + base_path: P, + filename: S, +) -> Result> { + // If there is a `StepIndex` component, append the index to the output filename + let step_index = get_export_sequence_index(data).ok(); + + let mut filename = filename.into(); + if let Some(index) = step_index { + write!(filename, "_{}", index)?; + } + write!(filename, ".ply")?; + let ply_file_path = base_path.as_ref().join(&filename); + + // Create any necessary parent directories (otherwise creating file will fail) + if let Some(dir) = ply_file_path.parent() { + create_dir_all(dir)?; + } + + Ok(File::create(ply_file_path)?) +} + +impl System for PlyVolumeMesh2dOutput { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + let mesh_storage = data.get_component_storage::().borrow(); + let name_storage = data.get_component_storage::().borrow(); + + for (id, mesh) in mesh_storage.entity_component_iter() { + if let Some(name) = name_storage.get_component(*id) { + let ply_file = prepare_ply_file_for_writing(data, &self.base_path, format!("{}_volume2d", name))?; + let mut ply_writer = io::BufWriter::new(ply_file); + + let polymesh = build_polymesh_from_2d_volume_mesh(mesh)?; + write_polymesh_faces_to_ply_ascii(&mut ply_writer, &polymesh)?; + + ply_writer.flush()?; + } + } + + Ok(()) + } +} + +impl System for PlyFem2dOutput { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + let fe_models_2d = data + .get_component_storage::() + .borrow(); + let name_storage = data.get_component_storage::().borrow(); + + for (id, fe_model) in fe_models_2d.entity_component_iter() { + if let Some(name) = name_storage.get_component(*id) { + let ply_file = prepare_ply_file_for_writing(data, &self.base_path, format!("{}_fe_mesh2d", name))?; + let mut ply_writer = io::BufWriter::new(ply_file); + + let mut polymesh = build_polymesh_from_2d_model(&fe_model)?; + interpolate_points_from_2d_model(polymesh.vertices_mut(), &fe_model)?; + write_polymesh_faces_to_ply_ascii(&mut ply_writer, &polymesh)?; + + ply_writer.flush()?; + } + } + + Ok(()) + } +} + +impl System for PlyInterpolatedPoints2dOutput { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + let interpolators_2d = data.get_component_storage::().borrow(); + let fe_models_2d = data + .get_component_storage::() + .borrow(); + let name_storage = data.get_component_storage::().borrow(); + + for (id, pi) in interpolators_2d.entity_component_iter() { + if let Some(name) = name_storage.get_component(*id) { + if let Some(model) = fe_models_2d.get_component(*id) { + // Interpolate points with deformed mesh + let interpolated_points: Vec> = pi + .interpolator + .interpolate::(&model.u) + .into_iter() + .zip(pi.reference_points.iter()) + .map(|(u, p0)| (p0.coords + u).into()) + .collect(); + + let ply_file = prepare_ply_file_for_writing(data, &self.base_path, format!("{}_points", name))?; + let mut ply_writer = io::BufWriter::new(ply_file); + + let pointcloud_mesh = build_degenerate_pointcloud_mesh_2d(interpolated_points.as_slice()); + let polymesh = Mesh::try_clone_face_soup_from_mesh(&pointcloud_mesh)?; + write_polymesh_faces_to_ply_ascii(&mut ply_writer, &polymesh)?; + + ply_writer.flush()?; + } + } + } + + Ok(()) + } +} + +/* +impl System for PlyFem3dOutput { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + let fe_models_3d = data + .get_component_storage::() + .borrow(); + let name_storage = data.get_component_storage::().borrow(); + + for (id, fe_model) in fe_models_3d.entity_component_iter() { + if let Some(name) = name_storage.get_component(*id) { + let ply_file = prepare_ply_file_for_writing( + data, + &self.base_path, + format!("{}_fe_mesh3d", name), + )?; + let mut ply_writer = io::BufWriter::new(ply_file); + + let mut polymesh = build_polymesh_from_3d_model(&fe_model)?; + interpolate_points_from_3d_model(polymesh.vertices_mut(), &fe_model)?; + write_polymesh_faces_to_ply_ascii(&mut ply_writer, &polymesh)?; + + ply_writer.flush()?; + } + } + + Ok(()) + } +} +*/ + +impl System for PlyPolyMesh2dOutput { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + let polymeshes_2d = data + .get_component_storage::() + .borrow(); + let fe_models_2d = data + .get_component_storage::() + .borrow(); + let name_storage = data.get_component_storage::().borrow(); + + for (id, p2d_collection) in polymeshes_2d.entity_component_iter() { + if let Some(name) = name_storage.get_component(*id) { + for p2d_component in p2d_collection.iter() { + // Optionally add subfolder to base output path + let mesh_path = if let Some(subfolder) = &p2d_component.subfolder { + self.base_path.join(subfolder) + } else { + self.base_path.clone() + }; + + // Recursively create folders and open file for writing + let ply_file = prepare_ply_file_for_writing( + data, + &mesh_path, + format!("{}_{}_polymesh", name, p2d_component.mesh_name), + )?; + + if let Some(interpolator) = &p2d_component.interpolator { + let mut polymesh = p2d_component.mesh.clone(); + + // Try to get FE model and interpolate vertices of mesh + if let Some(model) = fe_models_2d.get_component(*id) { + let u_interpolated = interpolator.interpolate::(&model.u); + + // Apply deformation to mesh + for (v, u) in polymesh + .vertices_mut() + .iter_mut() + .zip(u_interpolated.iter()) + { + v.coords += u; + } + }; + + let mut ply_writer = io::BufWriter::new(ply_file); + write_polymesh_faces_to_ply_ascii(&mut ply_writer, &polymesh)?; + ply_writer.flush()?; + } else { + let mut ply_writer = io::BufWriter::new(ply_file); + write_polymesh_faces_to_ply_ascii(&mut ply_writer, &p2d_component.mesh)?; + ply_writer.flush()?; + } + } + } + } + + Ok(()) + } +} + +impl System for PlyPolyMesh3dOutput { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + let polymeshes_3d = data + .get_component_storage::() + .borrow(); + let fe_models_3d = data + .get_component_storage::() + .borrow(); + let name_storage = data.get_component_storage::().borrow(); + + for (id, p3d_collection) in polymeshes_3d.entity_component_iter() { + if let Some(name) = name_storage.get_component(*id) { + for p3d_component in p3d_collection.iter() { + // Optionally add subfolder to base output path + let mesh_path = if let Some(subfolder) = &p3d_component.subfolder { + self.base_path.join(subfolder) + } else { + self.base_path.clone() + }; + + // Recursively create folders and open file for writing + let ply_file = prepare_ply_file_for_writing( + data, + &mesh_path, + format!("{}_{}_polymesh", name, p3d_component.mesh_name), + )?; + + if let Some(interpolator) = &p3d_component.interpolator { + let mut polymesh = p3d_component.mesh.clone(); + + // Try to get FE model and interpolate vertices of mesh + if let Some(model) = fe_models_3d.get_component(*id) { + let u_interpolated = interpolator.interpolate::(&model.u); + + // Apply deformation to mesh + for (v, u) in polymesh + .vertices_mut() + .iter_mut() + .zip(u_interpolated.iter()) + { + v.coords += u; + } + }; + + let mut ply_writer = io::BufWriter::new(ply_file); + write_polymesh_faces_to_ply_ascii(&mut ply_writer, &polymesh)?; + ply_writer.flush()?; + } else { + let mut ply_writer = io::BufWriter::new(ply_file); + write_polymesh_faces_to_ply_ascii(&mut ply_writer, &p3d_component.mesh)?; + ply_writer.flush()?; + } + } + } + } + + Ok(()) + } +} + +trait TryPolygonalizeFaceSoupFromMesh +where + T: Scalar, + D: DimName, + C: Connectivity, + DefaultAllocator: Allocator, +{ + fn try_polygonalize_face_soup_from_mesh(mesh: &Mesh) -> Result, Box> { + Ok(PolyMesh::from_surface_mesh(mesh)) + } +} + +impl TryPolygonalizeFaceSoupFromMesh for Mesh +where + T: RealField, + D: DimName, + C: Connectivity, + DefaultAllocator: Allocator, +{ +} + +trait TryCloneFaceSoupFromMesh +where + T: RealField, + D: DimName, + C: Connectivity, + DefaultAllocator: Allocator, +{ + fn try_clone_face_soup_from_mesh(mesh: &Mesh) -> Result, Box> { + let vertices = mesh.vertices().to_vec(); + let mut faces = NestedVec::new(); + let cells = NestedVec::new(); + + for cell in mesh.connectivity() { + faces.push(cell.vertex_indices()); + } + + Ok(PolyMesh::from_poly_data(vertices, faces, cells)) + } +} + +impl TryCloneFaceSoupFromMesh for Mesh2d {} + +impl TryCloneFaceSoupFromMesh for Mesh2d {} + +/// Creates a PolyMesh from the FEM mesh of a FiniteElementElasticModel2d +#[rustfmt::skip] +fn build_polymesh_from_2d_model( + elastic_model: &FiniteElementElasticModel2d, +) -> Result, Box> { + use FiniteElementModel2d::*; + match elastic_model.model { + Tri3d2NodalModel(ref model) => Mesh::try_clone_face_soup_from_mesh(model.mesh()), + Tri6d2NodalModel(ref model) => Mesh::try_polygonalize_face_soup_from_mesh(model.mesh()), + Quad4NodalModel(ref model) => Mesh::try_clone_face_soup_from_mesh(model.mesh()), + Quad9NodalModel(ref model) => Mesh::try_polygonalize_face_soup_from_mesh(model.mesh()), + EmbeddedTri3d2Model(ref model) => Mesh::try_clone_face_soup_from_mesh(model.background_mesh()), + EmbeddedTri6d2Model(ref model) => Mesh::try_polygonalize_face_soup_from_mesh(model.background_mesh()), + EmbeddedQuad4Model(ref model) => Mesh::try_clone_face_soup_from_mesh(model.background_mesh()), + EmbeddedQuad9Model(ref model) => Mesh::try_polygonalize_face_soup_from_mesh(model.background_mesh()), + } +} + +/// Creates a PolyMesh from a VolumeMesh2d +#[rustfmt::skip] +fn build_polymesh_from_2d_volume_mesh( + volume_mesh: &VolumeMesh2d, +) -> Result, Box> { + match volume_mesh { + VolumeMesh2d::QuadMesh(mesh) => Mesh::try_clone_face_soup_from_mesh(mesh), + VolumeMesh2d::TriMesh(mesh) => Mesh::try_clone_face_soup_from_mesh(mesh), + } +} + +/// Interpolate deformation of a FiniteElementElasticModel2d onto a list of points +fn interpolate_points_from_2d_model( + vertices: &mut [Point2], + elastic_model: &FiniteElementElasticModel2d, +) -> Result<(), Box> { + let interpolator = match_on_finite_element_model_2d!(elastic_model.model, model => { + model.make_interpolator(vertices)? + }); + let u_interpolated = interpolator.interpolate::(&elastic_model.u); + for (v, u) in vertices.iter_mut().zip(u_interpolated.iter()) { + v.coords += u; + } + + Ok(()) +} + +/// Creates a degenerate triangle soup from a point cloud +fn build_degenerate_pointcloud_mesh_2d(points: &[Point2]) -> TriangleMesh2d { + let mut vertices = Vec::with_capacity(points.len() * 3); + for p in points { + vertices.push(p.clone()); + vertices.push(p.clone()); + vertices.push(p.clone()); + } + + let mut connectivity = Vec::with_capacity(points.len()); + for i in 0..points.len() { + connectivity.push(Tri3d2Connectivity([3 * i + 0, 3 * i + 1, 3 * i + 2])); + } + + TriangleMesh2d::from_vertices_and_connectivity(vertices, connectivity) +} + +/* +/// Creates a PolyMesh from the FEM mesh of a FiniteElementElasticModel2d +#[rustfmt::skip] +fn build_polymesh_from_3d_model( + elastic_model: &FiniteElementElasticModel3d, +) -> Result, Box> { + use FiniteElementModel3d::*; + Ok(match elastic_model.model { + Hex8NodalModel(ref model) => PolyMesh::from(model.mesh()), + Tet4NodalModel(ref model) => PolyMesh::from(model.mesh()), + Tet10NodalModel(ref model) => PolyMesh::from(model.mesh()), + EmbeddedHex8Model(ref model) => PolyMesh::from(model.background_mesh()), + EmbeddedTet4Model(ref model) => PolyMesh::from(model.background_mesh()), + EmbeddedTet10Model(ref model) => PolyMesh::from(model.background_mesh()), + }) +} +*/ + +/* +/// Interpolate deformation of a FiniteElementElasticModel3d onto a list of points +fn interpolate_points_from_3d_model( + vertices: &mut [Point3], + elastic_model: &FiniteElementElasticModel3d, +) -> Result<(), Box> { + let interpolator = match_on_finite_element_model_3d!(elastic_model.model, model => { + model.make_interpolator(vertices)? + }); + let u_interpolated = interpolator.interpolate::(&elastic_model.u); + for (v, u) in vertices.iter_mut().zip(u_interpolated.iter()) { + v.coords += u; + } + + Ok(()) +} +*/ diff --git a/simulation_toolbox/src/io/vtk.rs b/simulation_toolbox/src/io/vtk.rs new file mode 100644 index 0000000..6ecf0bc --- /dev/null +++ b/simulation_toolbox/src/io/vtk.rs @@ -0,0 +1,264 @@ +use crate::components::{get_export_sequence_index, Name, SurfaceMesh2d, VolumeMesh2d, VolumeMesh3d}; +use crate::fem::{ + FiniteElementElasticModel2d, FiniteElementElasticModel3d, FiniteElementModel2d, FiniteElementModel3d, +}; +use fenris::connectivity::Segment2d2Connectivity; +use fenris::geometry::vtk::write_vtk; +use fenris::mesh::Mesh2d; +use fenris::nalgebra::DVector; +use fenris::solid::ElasticityModel; +use fenris::vtkio::model::{Attribute, DataSet}; +use fenris::vtkio::IOBuffer; +use hamilton::{StorageContainer, System}; +use log::warn; +use std::error::Error; +use std::fmt; +use std::fmt::Display; +use std::path::PathBuf; + +/// A system that writes named volume meshes to VTK files. +#[derive(Debug)] +pub struct VtkVolumeMeshOutput { + pub base_path: PathBuf, +} + +/// A system that writes named surface meshes to VTK files. +#[derive(Debug)] +pub struct VtkSurfaceMeshOutput { + pub base_path: PathBuf, +} + +#[derive(Debug)] +pub struct VtkFemOutput { + pub base_path: PathBuf, +} + +impl Display for VtkVolumeMeshOutput { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "VtkVolumeMeshOutput") + } +} + +impl Display for VtkSurfaceMeshOutput { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "VtkSurfaceMeshOutput") + } +} + +impl Display for VtkFemOutput { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "VtkFemOutput") + } +} + +impl System for VtkVolumeMeshOutput { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + // If there is a `StepIndex` component, append the index to the output filename + let step_index = get_export_sequence_index(data).ok(); + let mesh2d_storage = data.get_component_storage::().borrow(); + let mesh3d_storage = data.get_component_storage::().borrow(); + let name_storage = data.get_component_storage::().borrow(); + + for (id, mesh) in mesh2d_storage.entity_component_iter() { + if let Some(name) = name_storage.get_component(*id) { + let filename = create_vtk_filename(&name.0, "volume_mesh", step_index); + let vtk_file_path = self.base_path.join(filename); + + let dataset = match mesh { + VolumeMesh2d::QuadMesh(ref mesh) => DataSet::from(mesh), + VolumeMesh2d::TriMesh(ref mesh) => DataSet::from(mesh), + }; + + write_vtk(dataset, vtk_file_path, &name.0)?; + } + } + + for (id, mesh) in mesh3d_storage.entity_component_iter() { + if let Some(name) = name_storage.get_component(*id) { + if mesh2d_storage.get_component(*id).is_some() { + warn!( + "Component has both 2D and 3D volume mesh.\ + 2D volume mesh output will be overwritten." + ); + } + + let filename = create_vtk_filename(&name.0, "volume_mesh", step_index); + let vtk_file_path = self.base_path.join(filename); + + let dataset = DataSet::from(&mesh.0); + write_vtk(dataset, vtk_file_path, &name.0)?; + } + } + + Ok(()) + } +} + +/// Creates a mesh containing line segments corresponding to the normals of the surface mesh, +/// with normals placed at the midpoint of the segments. +fn create_normals_mesh(surface_mesh: &Mesh2d) -> Mesh2d { + let mut vertices = Vec::new(); + let mut connectivity = Vec::new(); + + for cell in surface_mesh.cell_iter() { + let begin_idx = vertices.len(); + let end_idx = begin_idx + 1; + let midpoint = cell.point_from_parameter(0.5); + + let normal_begin = midpoint; + let normal_end = midpoint + cell.normal_dir().normalize(); + vertices.push(normal_begin); + vertices.push(normal_end); + connectivity.push(Segment2d2Connectivity([begin_idx, end_idx])); + } + + Mesh2d::from_vertices_and_connectivity(vertices, connectivity) +} + +fn create_vtk_filename(object_name: &str, tag: &str, step_index: Option) -> String { + format!( + "{obj}_{tag}{step_index}.vtk", + obj = object_name, + tag = tag, + step_index = step_index + .map(|i| format!("_{}", i)) + .unwrap_or(String::new()) + ) +} + +impl System for VtkSurfaceMeshOutput { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + // If there is a `StepIndex` component, append the index to the output filename + let step_index = get_export_sequence_index(data).ok(); + let mesh_storage = data.get_component_storage::().borrow(); + let name_storage = data.get_component_storage::().borrow(); + + for (id, mesh) in mesh_storage.entity_component_iter() { + if let Some(name) = name_storage.get_component(*id) { + // Surface + let surface_filename = create_vtk_filename(&name.0, "surface_mesh", step_index); + let surface_vtk_file_path = self.base_path.join(surface_filename); + let surface_dataset = DataSet::from(&mesh.0); + write_vtk( + surface_dataset, + surface_vtk_file_path, + &format!("{} surface mesh", &name.0), + )?; + + // Export normals + let normals_filename = create_vtk_filename(&name.0, "normals", step_index); + let normals_vtk_file_path = self.base_path.join(normals_filename); + let normals_dataset = DataSet::from(&create_normals_mesh(&mesh.0)); + write_vtk(normals_dataset, normals_vtk_file_path, &format!("{} normals", &name.0))?; + } + } + + Ok(()) + } +} + +fn create_fem_dataset_2d(model: &FiniteElementModel2d) -> DataSet { + use FiniteElementModel2d::*; + match model { + Tri3d2NodalModel(ref model) => DataSet::from(model.mesh()), + Tri6d2NodalModel(ref model) => DataSet::from(model.mesh()), + Quad4NodalModel(ref model) => DataSet::from(model.mesh()), + Quad9NodalModel(ref model) => DataSet::from(model.mesh()), + EmbeddedTri3d2Model(ref model) => DataSet::from(model.background_mesh()), + EmbeddedTri6d2Model(ref model) => DataSet::from(model.background_mesh()), + EmbeddedQuad4Model(ref model) => DataSet::from(model.background_mesh()), + EmbeddedQuad9Model(ref model) => DataSet::from(model.background_mesh()), + } +} + +fn create_fem_dataset_3d(model: &FiniteElementModel3d) -> DataSet { + use FiniteElementModel3d::*; + match model { + Hex8NodalModel(ref model) => DataSet::from(model.mesh()), + Hex20NodalModel(ref model) => DataSet::from(model.mesh()), + Hex27NodalModel(ref model) => DataSet::from(model.mesh()), + Tet4NodalModel(ref model) => DataSet::from(model.mesh()), + Tet10NodalModel(ref model) => DataSet::from(model.mesh()), + EmbeddedHex8Model(ref model) => DataSet::from(model.background_mesh()), + EmbeddedHex20Model(ref model) => DataSet::from(model.background_mesh()), + EmbeddedHex27Model(ref model) => DataSet::from(model.background_mesh()), + EmbeddedTet4Model(ref model) => DataSet::from(model.background_mesh()), + EmbeddedTet10Model(ref model) => DataSet::from(model.background_mesh()), + } +} + +impl System for VtkFemOutput { + fn run(&mut self, data: &StorageContainer) -> Result<(), Box> { + // If there is a `StepIndex` component, append the index to the output filename + let step_index = get_export_sequence_index(data).ok(); + let fe_models_2d = data + .get_component_storage::() + .borrow(); + let fe_models_3d = data + .get_component_storage::() + .borrow(); + let name_storage = data.get_component_storage::().borrow(); + + for (id, fe_model) in fe_models_2d.entity_component_iter() { + if let Some(name) = name_storage.get_component(*id) { + let mesh_filename = create_vtk_filename(&name.0, "fe_mesh", step_index); + let mesh_vtk_file_path = self.base_path.join(mesh_filename); + let mut mesh_dataset = create_fem_dataset_2d(&fe_model.model); + + if let DataSet::UnstructuredGrid { ref mut data, .. } = mesh_dataset { + // Displacements are 2d, but Paraview needs 3d points + let num_dofs_3d = 3 * fe_model.model.ndof() / 2; + let displacements_3d = DVector::from_fn(num_dofs_3d, |i, _| { + let node_index = i / 3; + let local_index = i % 3; + if local_index < 2 { + fe_model.u[2 * node_index + local_index] + } else { + 0.0 + } + }); + + let displacement_buffer = IOBuffer::from_slice(displacements_3d.as_slice()); + let attribute = Attribute::Vectors { + data: displacement_buffer, + }; + data.point.push((format!("displacement"), attribute)); + } + + write_vtk( + mesh_dataset, + mesh_vtk_file_path, + &format!("{} Finite Element mesh", &name.0), + )?; + } + } + + for (id, fe_model) in fe_models_3d.entity_component_iter() { + if let Some(name) = name_storage.get_component(*id) { + if let Some(_) = fe_models_2d.get_component(*id) { + warn!("Entity has both 2d and 3d FEM model. Naming conflict for output."); + } + + let mesh_filename = create_vtk_filename(&name.0, "fe_mesh", step_index); + let mesh_vtk_file_path = self.base_path.join(mesh_filename); + let mut mesh_dataset = create_fem_dataset_3d(&fe_model.model); + + if let DataSet::UnstructuredGrid { ref mut data, .. } = mesh_dataset { + let displacement_buffer = IOBuffer::from_slice(fe_model.u.as_slice()); + let attribute = Attribute::Vectors { + data: displacement_buffer, + }; + data.point.push((format!("displacement"), attribute)); + } + + write_vtk( + mesh_dataset, + mesh_vtk_file_path, + &format!("{} Finite Element mesh", &name.0), + )?; + } + } + + Ok(()) + } +} diff --git a/simulation_toolbox/src/lib.rs b/simulation_toolbox/src/lib.rs new file mode 100644 index 0000000..b6722fc --- /dev/null +++ b/simulation_toolbox/src/lib.rs @@ -0,0 +1,8 @@ +// TODO: Instead of organizing things into modules for components, systems etc., +// instead group related things together, such as VTK export components and systems together + +#[macro_use] +pub mod fem; +pub mod components; +pub mod io; +pub mod util; diff --git a/simulation_toolbox/src/util.rs b/simulation_toolbox/src/util.rs new file mode 100644 index 0000000..ca1cf05 --- /dev/null +++ b/simulation_toolbox/src/util.rs @@ -0,0 +1,71 @@ +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::hash::Hash; + +use fenris::nalgebra::allocator::Allocator; +use fenris::nalgebra::{DefaultAllocator, DimName, Point, RealField, VectorN}; +use itertools::izip; + +pub trait IfTrue { + fn if_true(&self, then_some: T) -> Option; +} + +impl IfTrue for bool { + /// Maps `true` to a `Some(then_some)` or `false` to `None`. + fn if_true(&self, then_some: T) -> Option { + if *self { + Some(then_some) + } else { + None + } + } +} + +// TODO: Move this somewhere +pub fn apply_displacements(x: &mut [Point], x0: &[Point], displacements: &[VectorN]) +where + T: RealField, + D: DimName, + DefaultAllocator: Allocator, +{ + assert_eq!(x.len(), x0.len()); + assert_eq!(x0.len(), displacements.len()); + for (v, v0, d) in izip!(x, x0, displacements) { + *v = v0 + d; + } +} + +/// Takes an iterator of indices that yields unsorted, possibly duplicate indices +/// and maps the indices to a new set of indices [0, N), where `N` is the number of +/// unique original indices. +/// +/// Returns a tuple consisting of the number of indices in the new index set and +/// a mapping from old to new indices. +pub fn relabel_indices(original_indices: impl IntoIterator) -> (usize, HashMap) { + let iter = original_indices.into_iter(); + let ordered_indices: BTreeSet<_> = iter.collect(); + let num_new_indices = ordered_indices.len(); + let mapping = ordered_indices + .into_iter() + .enumerate() + .map(|(new_idx, old_idx)| (old_idx, new_idx)) + .collect(); + + (num_new_indices, mapping) +} + +pub fn difference(a: impl Iterator, b: impl Iterator) -> Vec { + let set_a: HashSet<_> = a.collect(); + let set_b: HashSet<_> = b.collect(); + set_a.difference(&set_b).map(|v| (*v).clone()).collect() +} + +pub fn intersection(a: impl Iterator, b: impl Iterator) -> Vec { + let set_a: HashSet<_> = a.collect(); + let set_b: HashSet<_> = b.collect(); + set_a.intersection(&set_b).map(|v| (*v).clone()).collect() +} + +pub fn all_items_unique(iter: impl IntoIterator) -> bool { + let mut uniq = HashSet::new(); + iter.into_iter().all(move |x| uniq.insert(x)) +}