diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2a089d1e..192888b4 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -35,6 +35,7 @@ jobs: # `-C link-dead-code` is needed to prevent 'warning: XX functions have mismatched data' warnings RUSTFLAGS: '-Cinstrument-coverage -Clink-dead-code' run: | + export NEOPDF_DATA_PATH=/usr/local/share/LHAPDF # we need stderr, but we can't run test twice because it'll regenerate/modify the binaries which interferes with `llvm-cov` cargo test --features=applgrid,evolve,fastnlo,fktable --no-fail-fast 2> >(tee stderr 1>&2) # from https://stackoverflow.com/a/51141872/812178 diff --git a/CHANGELOG.md b/CHANGELOG.md index df97b4e6..eb9f8d2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Isolate PDF interpolation backend, current suppored backends include + LHAPDF and NeoPDF + ## [1.4.0] - 12/05/2026 Starting with this version, PineAPPL has an official logo! diff --git a/Cargo.lock b/Cargo.lock index 433a15d4..cb5b4fca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,10 +3,10 @@ version = 4 [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" @@ -172,6 +172,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + [[package]] name = "byteorder" version = "1.5.0" @@ -252,7 +258,7 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ "serde", "termcolor", - "unicode-width", + "unicode-width 0.1.11", ] [[package]] @@ -261,6 +267,19 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.2", + "windows-sys 0.59.0", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -451,6 +470,12 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.15.0" @@ -520,9 +545,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.0.28" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -549,6 +574,30 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -675,6 +724,19 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width 0.2.2", + "web-time", +] + [[package]] name = "is-terminal" version = "0.4.17" @@ -688,9 +750,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.14.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -701,6 +763,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -737,12 +811,27 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lz4_flex" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" +dependencies = [ + "twox-hash", +] + [[package]] name = "lz4_flex" version = "0.13.0" @@ -790,11 +879,28 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ - "adler", + "adler2", + "simd-adler32", +] + +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", + "serde", ] [[package]] @@ -820,12 +926,74 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58e8a348bca0075000d999d750420d74434fd0d3e0993b456554f885e7657a11" dependencies = [ "byteorder", - "ndarray", + "ndarray 0.17.2", "num-traits", "py_literal", "zip", ] +[[package]] +name = "neopdf" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3960d3d8f81b41ee953c7d899491c5588454bbfa442ccb88547c0672c28bc03" +dependencies = [ + "bincode", + "flate2", + "git-version", + "indicatif", + "itertools", + "lz4_flex 0.11.6", + "ndarray 0.16.1", + "neopdf_legacy", + "ninterp", + "rayon", + "regex", + "serde", + "serde_yaml", + "tar", + "tempfile", + "thiserror", + "ureq", +] + +[[package]] +name = "neopdf_legacy" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1377fafc63af93eb63758599b8330174734aaaa9e41c38a47c4e11a735b1d76f" +dependencies = [ + "bincode", + "flate2", + "git-version", + "indicatif", + "itertools", + "lz4_flex 0.11.6", + "ndarray 0.16.1", + "ninterp", + "rayon", + "regex", + "serde", + "serde_yaml", + "tar", + "tempfile", + "thiserror", + "ureq", +] + +[[package]] +name = "ninterp" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b9a8917fb3e2ae0574a15c14f29d1855cb53f483edfde819bc218238830ca11" +dependencies = [ + "dyn-clone", + "itertools", + "ndarray 0.16.1", + "num-traits", + "thiserror", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -863,6 +1031,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "numpy" version = "0.28.0" @@ -870,7 +1044,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778da78c64ddc928ebf5ad9df5edf0789410ff3bdbf3619aed51cd789a6af1e2" dependencies = [ "libc", - "ndarray", + "ndarray 0.17.2", "num-complex", "num-integer", "num-traits", @@ -940,6 +1114,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "pineappl" version = "1.4.0" @@ -952,9 +1132,9 @@ dependencies = [ "float-cmp", "git-version", "itertools", - "lz4_flex", + "lz4_flex 0.13.0", "managed-lhapdf", - "ndarray", + "ndarray 0.17.2", "num-complex", "pineappl_v0", "rand", @@ -981,7 +1161,7 @@ name = "pineappl_capi" version = "1.4.0" dependencies = [ "itertools", - "ndarray", + "ndarray 0.17.2", "pineappl", ] @@ -1001,10 +1181,11 @@ dependencies = [ "float-cmp", "git-version", "itertools", - "lz4_flex", + "lz4_flex 0.13.0", "managed-lhapdf", - "ndarray", + "ndarray 0.17.2", "ndarray-npy", + "neopdf", "pineappl", "pineappl_applgrid", "pineappl_fastnlo", @@ -1031,7 +1212,7 @@ name = "pineappl_py" version = "1.4.0" dependencies = [ "itertools", - "ndarray", + "ndarray 0.17.2", "numpy", "pineappl", "pyo3", @@ -1043,7 +1224,7 @@ version = "1.4.0" dependencies = [ "bincode", "enum_dispatch", - "ndarray", + "ndarray 0.17.2", "serde", "thiserror", ] @@ -1106,7 +1287,7 @@ dependencies = [ "is-terminal", "lazy_static", "term", - "unicode-width", + "unicode-width 0.1.11", ] [[package]] @@ -1268,6 +1449,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -1320,7 +1513,20 @@ dependencies = [ "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", "windows-sys 0.52.0", ] @@ -1455,6 +1661,18 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" @@ -1492,6 +1710,7 @@ checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", + "xattr", ] [[package]] @@ -1509,7 +1728,7 @@ dependencies = [ "cfg-if", "fastrand", "once_cell", - "rustix", + "rustix 0.38.44", "windows-sys 0.52.0", ] @@ -1623,6 +1842,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -1707,6 +1932,61 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.26.1" @@ -1738,7 +2018,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1765,6 +2045,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1895,6 +2184,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.4", +] + [[package]] name = "xtask" version = "1.4.0" diff --git a/Cargo.toml b/Cargo.toml index 3eaed0d9..3ffe8a1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,11 +35,12 @@ enum_dispatch = "0.3.7" flate2 = "1.0.22" float-cmp = { default-features = false, version = "0.10.0" } git-version = "0.3.5" -itertools = "0.14.0" +itertools = "0.13.0" lhapdf = { package = "managed-lhapdf", version = "0.4.1" } lz4_flex = "0.13.0" ndarray = { version = "0.17.2" } ndarray-npy = { default-features = false, features = ["npz"], version = "0.10.0" } +neopdf = "0.3.3" num-complex = "0.4.4" numpy = "0.28.0" pkg-config = "0.3.26" diff --git a/README.md b/README.md index 08cbc86e..3fa243d7 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ If you use PineAPPL, please cite By using PineAPPL, you're probably also using - [APPLgrid], -- [fastNLO] and/or +- [fastNLO], +- [NeoPDF] and/or - [LHAPDF]. If that is the case, please cite these accordingly. @@ -53,6 +54,7 @@ If that is the case, please cite these accordingly. [APPLgrid]: https://applgrid.hepforge.org [fastNLO]: https://fastnlo.hepforge.org [LHAPDF]: https://lhapdf.hepforge.org +[NeoPDF]: https://qcdlab.github.io/neopdf/ [zenodo DOI]: https://zenodo.org/badge/latestdoi/248306479 [paper]: https://inspirehep.net/literature/1814432 diff --git a/pineappl_cli/Cargo.toml b/pineappl_cli/Cargo.toml index 65e8041e..854cca95 100644 --- a/pineappl_cli/Cargo.toml +++ b/pineappl_cli/Cargo.toml @@ -30,6 +30,7 @@ lhapdf.workspace = true lz4_flex = { optional = true, workspace = true } ndarray.workspace = true ndarray-npy = { optional = true, workspace = true } +neopdf.workspace = true pineappl.workspace = true pineappl_applgrid = { optional = true, workspace = true } pineappl_fastnlo = { optional = true, workspace = true } diff --git a/pineappl_cli/src/convolve.rs b/pineappl_cli/src/convolve.rs index f6ceae11..78728562 100644 --- a/pineappl_cli/src/convolve.rs +++ b/pineappl_cli/src/convolve.rs @@ -58,10 +58,11 @@ pub struct Opts { impl Subcommand for Opts { fn run(&self, cfg: &GlobalConfiguration) -> Result { let grid = helpers::read_grid(&self.input)?; - let mut conv_funs_0 = helpers::create_conv_funs(&self.conv_funs[0])?; + let mut conv_funs_0 = + helpers::create_conv_funs_with_backend(&self.conv_funs[0], cfg.pdf_backend)?; let bins: Vec<_> = self.bins.iter().cloned().flatten().collect(); - let results = helpers::convolve_scales( + let results = helpers::convolve_scales_with_backend( &grid, &mut conv_funs_0, &self.conv_funs[0].conv_types, @@ -91,8 +92,9 @@ impl Subcommand for Opts { .iter() .flat_map(|conv_funs| { let conv_types = &conv_funs.conv_types; - let mut conv_funs = helpers::create_conv_funs(conv_funs).unwrap(); - helpers::convolve( + let mut conv_funs = + helpers::create_conv_funs_with_backend(conv_funs, cfg.pdf_backend).unwrap(); + helpers::convolve_with_backend( &grid, &mut conv_funs, conv_types, diff --git a/pineappl_cli/src/helpers.rs b/pineappl_cli/src/helpers.rs index 6a075622..53a092ea 100644 --- a/pineappl_cli/src/helpers.rs +++ b/pineappl_cli/src/helpers.rs @@ -1,4 +1,5 @@ use super::GlobalConfiguration; +use super::pdf_backend::{self, Backend, ForcePositive, PdfBackend, PdfSetBackend}; use anyhow::{Context, Error, Result, anyhow, bail}; use itertools::Itertools; use lhapdf::{Pdf, PdfSet}; @@ -73,6 +74,7 @@ impl FromStr for ConvFuns { } } +/// Creates convolution functions using LHAPDF backend (legacy). pub fn create_conv_funs(funs: &ConvFuns) -> Result> { Ok(funs .lhapdf_names @@ -91,6 +93,22 @@ pub fn create_conv_funs(funs: &ConvFuns) -> Result> { .collect::>()?) } +/// Creates convolution functions using the specified backend. +pub fn create_conv_funs_with_backend( + funs: &ConvFuns, + backend: Backend, +) -> Result>> { + funs.lhapdf_names + .iter() + .zip(&funs.members) + .map(|(name, member)| { + let member = member.unwrap_or(0); + pdf_backend::create_pdf(name, member, backend) + }) + .collect() +} + +/// Creates convolution functions for a PDF set using LHAPDF backend (legacy). pub fn create_conv_funs_for_set( funs: &ConvFuns, index_of_set: usize, @@ -124,6 +142,30 @@ pub fn create_conv_funs_for_set( Ok((set, conv_funs)) } +/// Creates convolution functions for a PDF set using the specified backend. +/// +/// Returns a tuple of (`PdfSetBackend`, Vec>>). +pub fn create_conv_funs_for_set_with_backend( + funs: &ConvFuns, + index_of_set: usize, + backend: Backend, +) -> Result<(Box, Vec>>)> { + let setname = &funs.lhapdf_names[index_of_set]; + let set = pdf_backend::create_pdf_set(setname, backend)?; + + let set_members = set.mk_pdfs()?; + let conv_funs = set_members + .into_iter() + .map(|member_pdf| { + let mut conv_funs = create_conv_funs_with_backend(funs, backend)?; + conv_funs[index_of_set] = member_pdf; + Ok::<_, Error>(conv_funs) + }) + .collect::>()?; + + Ok((set, conv_funs)) +} + pub fn read_grid(input: &Path) -> Result { Grid::read(File::open(input).context(format!("unable to open '{}'", input.display()))?) .context(format!("unable to read '{}'", input.display())) @@ -247,6 +289,7 @@ pub enum ConvoluteMode { Normal, } +/// Performs convolution with scale variations using LHAPDF backend (legacy). pub fn convolve_scales( grid: &Grid, conv_funs: &mut [Pdf], @@ -327,6 +370,125 @@ pub fn convolve_scales( let mut cache = ConvolutionCache::new(convolutions, xfx, &mut alphas_funs[cfg.use_alphas_from]); let mut results = grid.convolve(&mut cache, &orders, bins, channels, scales); + match mode { + ConvoluteMode::Asymmetry => { + let bin_count = grid.bwfl().len(); + assert!((bins.is_empty() || (bins.len() == bin_count)) && (bin_count % 2 == 0)); + + results + .iter() + .skip((bin_count / 2) * scales.len()) + .zip( + results + .chunks_exact(scales.len()) + .take(bin_count / 2) + .rev() + .flatten(), + ) + .map(|(pos, neg)| (pos - neg) / (pos + neg)) + .collect() + } + ConvoluteMode::Integrated => { + results + .iter_mut() + .zip( + grid.bwfl() + .normalizations() + .into_iter() + .enumerate() + .filter(|(index, _)| bins.is_empty() || bins.contains(index)) + .flat_map(|(_, norm)| iter::repeat(norm).take(scales.len())), + ) + .for_each(|(value, norm)| *value *= norm); + + results + } + ConvoluteMode::Normal => results, + } +} + +/// Performs convolution with scale variations using the backend abstraction. +#[allow(clippy::too_many_arguments)] +pub fn convolve_scales_with_backend( + grid: &Grid, + conv_funs: &mut [Box], + conv_types: &[ConvType], + orders: &[(u8, u8)], + bins: &[usize], + channels: &[bool], + scales: &[(f64, f64, f64)], + mode: ConvoluteMode, + cfg: &GlobalConfiguration, +) -> Vec { + let orders: Vec<_> = grid + .orders() + .iter() + .map(|order| { + orders.is_empty() + || orders + .iter() + .any(|other| (order.alphas == other.0) && (order.alpha == other.1)) + }) + .collect(); + + if cfg.force_positive { + for fun in conv_funs.iter_mut() { + fun.set_force_positive(ForcePositive::ClipNegative); + } + } + + // TODO: promote this to an error + assert!( + cfg.use_alphas_from < conv_funs.len(), + "expected `use_alphas_from` to be an integer within `[0, {})`, but got `{}`", + conv_funs.len(), + cfg.use_alphas_from + ); + + let x_min_max: Vec<_> = conv_funs + .iter_mut() + .map(|fun| (fun.x_min(), fun.x_max())) + .collect(); + + let mut funs: Vec<_> = conv_funs + .iter() + .zip(&x_min_max) + .map(|(fun, &(x_min, x_max))| { + move |id: i32, x: f64, q2: f64| { + if !cfg.allow_extrapolation && (x < x_min || x > x_max) { + 0.0 + } else { + fun.xfx_q2(id, x, q2) + } + } + }) + .collect(); + + let xfx: Vec<_> = funs + .iter_mut() + .map(|fun| fun as &mut dyn FnMut(i32, f64, f64) -> f64) + .collect(); + + let mut alphas_funs: Vec<_> = conv_funs + .iter() + .map(|fun| { + let fun_ref = fun.as_ref(); + move |q2: f64| fun_ref.alphas_q2(q2) + }) + .collect(); + + let convolutions: Vec<_> = conv_funs + .iter() + .zip(conv_types) + .map(|(fun, &conv_type)| { + let pid = fun.particle_id(); + Conv::new(conv_type, pid) + }) + .collect(); + + let mut cache = ConvolutionCache::new(convolutions, xfx, &mut alphas_funs[cfg.use_alphas_from]); + let mut results = grid.convolve(&mut cache, &orders, bins, channels, scales); + match mode { ConvoluteMode::Asymmetry => { let bin_count = grid.bwfl().len(); @@ -382,6 +544,7 @@ pub fn scales_vector(grid: &Grid, scales: usize) -> &[(f64, f64, f64)] { } } +/// Performs convolution using LHAPDF backend (legacy). pub fn convolve( grid: &Grid, conv_funs: &mut [Pdf], @@ -406,6 +569,32 @@ pub fn convolve( ) } +/// Performs convolution using the backend abstraction. +#[allow(clippy::too_many_arguments)] +pub fn convolve_with_backend( + grid: &Grid, + conv_funs: &mut [Box], + conv_types: &[ConvType], + orders: &[(u8, u8)], + bins: &[usize], + lumis: &[bool], + scales: usize, + mode: ConvoluteMode, + cfg: &GlobalConfiguration, +) -> Vec { + convolve_scales_with_backend( + grid, + conv_funs, + conv_types, + orders, + bins, + lumis, + scales_vector(grid, scales), + mode, + cfg, + ) +} + pub fn convolve_limits(grid: &Grid, bins: &[usize], mode: ConvoluteMode) -> Vec> { let limits: Vec<_> = grid .bwfl() diff --git a/pineappl_cli/src/lib.rs b/pineappl_cli/src/lib.rs index 04ee204b..166e3ee7 100644 --- a/pineappl_cli/src/lib.rs +++ b/pineappl_cli/src/lib.rs @@ -14,6 +14,7 @@ mod helpers; mod import; mod merge; mod orders; +pub mod pdf_backend; mod plot; mod pull; mod read; @@ -41,6 +42,9 @@ pub struct GlobalConfiguration { /// Choose the PDF/FF set for the strong coupling. #[arg(default_value = "0", long, value_name = "IDX")] pub use_alphas_from: usize, + /// Select the PDF interpolation backend: 'lhapdf' or 'neopdf'. + #[arg(default_value = "lhapdf", long, value_name = "BACKEND")] + pub pdf_backend: pdf_backend::Backend, } #[enum_dispatch] diff --git a/pineappl_cli/src/pdf_backend.rs b/pineappl_cli/src/pdf_backend.rs new file mode 100644 index 00000000..f5050582 --- /dev/null +++ b/pineappl_cli/src/pdf_backend.rs @@ -0,0 +1,432 @@ +//! PDF backend abstraction layer. +//! +//! This module provides a unified interface for different PDF interpolation backends, +//! currently supporting `LHAPDF` and `NeoPDF`. It allows runtime selection of the backend +//! and provides type-safe access to PDF metadata. + +use anyhow::{Context, Result, anyhow}; +use std::fmt; +use std::str::FromStr; + +pub use neopdf::uncertainty::CL_1_SIGMA; + +/// The type of PDF set (space-like for PDFs, time-like for FFs). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum SetType { + /// Space-like PDF (standard parton distribution function). + #[default] + SpaceLike, + /// Time-like PDF (fragmentation function). + TimeLike, +} + +impl fmt::Display for SetType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::SpaceLike => write!(f, "SpaceLike"), + Self::TimeLike => write!(f, "TimeLike"), + } + } +} + +/// Method for handling negative PDF values. +#[derive(Clone, Copy, Debug, Default)] +pub enum ForcePositive { + /// No clipping - return values as-is. + #[default] + None, + /// Clip negative values to zero. + ClipNegative, +} + +/// Available PDF backends. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Backend { + /// `LHAPDF` backend (C++ library with Rust bindings). + #[default] + Lhapdf, + /// `NeoPDF` backend (pure Rust implementation). + Neopdf, +} + +impl FromStr for Backend { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "lhapdf" => Ok(Self::Lhapdf), + "neopdf" => Ok(Self::Neopdf), + _ => Err(anyhow!( + "unknown PDF backend '{}'; must be 'lhapdf' or 'neopdf'", + s + )), + } + } +} + +impl fmt::Display for Backend { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Lhapdf => write!(f, "lhapdf"), + Self::Neopdf => write!(f, "neopdf"), + } + } +} + +/// Unified PDF backend interface. +/// +/// This trait provides a common interface for PDF interpolation backends, +/// allowing the CLI to work with different implementations transparently. +pub trait PdfBackend: Send { + /// Evaluates xf(x, Q^2) for a given flavor. + /// + /// # Arguments + /// * `id` - The Monte Carlo PDG flavor ID. + /// * `x` - The momentum fraction. + /// * `q2` - The squared energy scale. + /// + /// # Returns + /// The PDF value xf(x, Q^2). + /// + /// # TODO + /// Extend to support `NeoPDF` multi-parameters interpolation. + fn xfx_q2(&self, id: i32, x: f64, q2: f64) -> f64; + + /// Evaluates the strong coupling constant `alpha_s(Q^2)`. + fn alphas_q2(&self, q2: f64) -> f64; + + /// Returns the minimum valid x value. + fn x_min(&mut self) -> f64; + + /// Returns the maximum valid x value. + fn x_max(&mut self) -> f64; + + /// Returns the hadron particle ID (PDG code). + fn particle_id(&self) -> i32; + + /// Returns whether this is a polarized PDF. + fn is_polarized(&self) -> bool; + + /// Returns the PDF set type (space-like or time-like). + fn set_type(&self) -> SetType; + + /// Sets the method for handling negative PDF values. + fn set_force_positive(&mut self, method: ForcePositive); + + /// Returns the error type of the PDF set (e.g., "replicas", "hessian"). + fn error_type(&self) -> String; +} + +/// Unified PDF set interface for uncertainty calculations. +pub trait PdfSetBackend { + /// Returns the number of members in this set. + fn num_members(&self) -> usize; + + /// Creates all PDF members in the set. + fn mk_pdfs(&self) -> Result>>; + + /// Calculates the uncertainty for a set of values. + fn uncertainty(&self, values: &[f64], cl: f64, alternative: bool) -> Result; +} + +/// Uncertainty information from a PDF set. +#[derive(Clone, Debug)] +pub struct Uncertainty { + /// Central value. + pub central: f64, + /// Negative error (absolute value). + pub errminus: f64, + /// Positive error (absolute value). + pub errplus: f64, +} + +// ============================================================================ +// LHAPDF Backend Implementation +// ============================================================================ + +/// LHAPDF backend wrapper. +pub struct LhapdfPdf { + pdf: lhapdf::Pdf, +} + +impl LhapdfPdf { + /// Creates a new LHAPDF PDF by set name and member index. + pub fn with_setname_and_member(setname: &str, member: i32) -> Result { + Ok(Self { + pdf: lhapdf::Pdf::with_setname_and_member(setname, member).with_context(|| { + format!("failed to load LHAPDF set '{setname}' member {member}") + })?, + }) + } + + /// Creates a new LHAPDF PDF by LHAID. + pub fn with_lhaid(lhaid: i32) -> Result { + Ok(Self { + pdf: lhapdf::Pdf::with_lhaid(lhaid) + .with_context(|| format!("failed to load LHAPDF with LHAID {lhaid}"))?, + }) + } +} + +impl PdfBackend for LhapdfPdf { + fn xfx_q2(&self, id: i32, x: f64, q2: f64) -> f64 { + self.pdf.xfx_q2(id, x, q2) + } + + fn alphas_q2(&self, q2: f64) -> f64 { + self.pdf.alphas_q2(q2) + } + + fn x_min(&mut self) -> f64 { + self.pdf.x_min() + } + + fn x_max(&mut self) -> f64 { + self.pdf.x_max() + } + + fn particle_id(&self) -> i32 { + self.pdf + .set() + .entry("Particle") + .map_or(Ok(2212), |s| s.parse()) + .unwrap_or(2212) + } + + fn is_polarized(&self) -> bool { + self.pdf + .set() + .entry("Polarized") + .is_some_and(|s| s.eq_ignore_ascii_case("true")) + } + + fn set_type(&self) -> SetType { + self.pdf + .set() + .entry("SetType") + .map_or(SetType::SpaceLike, |s| { + if s.eq_ignore_ascii_case("timelike") { + SetType::TimeLike + } else { + SetType::SpaceLike + } + }) + } + + fn set_force_positive(&mut self, method: ForcePositive) { + match method { + ForcePositive::None => self.pdf.set_force_positive(0), + ForcePositive::ClipNegative => self.pdf.set_force_positive(1), + } + } + + fn error_type(&self) -> String { + self.pdf + .set() + .entry("ErrorType") + .map(|s| s.to_owned()) + .unwrap_or_default() + } +} + +/// LHAPDF set wrapper. +pub struct LhapdfSet { + set: lhapdf::PdfSet, +} + +impl LhapdfSet { + /// Creates a new LHAPDF set by name. + pub fn new(setname: &str) -> Result { + Ok(Self { + set: lhapdf::PdfSet::new(setname) + .with_context(|| format!("failed to load LHAPDF set '{setname}'"))?, + }) + } +} + +impl PdfSetBackend for LhapdfSet { + fn num_members(&self) -> usize { + self.set + .entry("NumMembers") + .and_then(|s| s.parse().ok()) + .unwrap_or(1) + } + + fn mk_pdfs(&self) -> Result>> { + let pdfs = self.set.mk_pdfs()?; + Ok(pdfs + .into_iter() + .map(|pdf| Box::new(LhapdfPdf { pdf }) as Box) + .collect()) + } + + fn uncertainty(&self, values: &[f64], cl: f64, alternative: bool) -> Result { + let unc = self.set.uncertainty(values, cl, alternative)?; + Ok(Uncertainty { + central: unc.central, + errminus: unc.errminus, + errplus: unc.errplus, + }) + } +} + +// ============================================================================ +// NeoPDF Backend Implementation +// ============================================================================ + +/// `NeoPDF` backend wrapper. +pub struct NeopdfPdf { + pdf: neopdf::pdf::PDF, +} + +impl NeopdfPdf { + /// Loads a NeoPDF member by set name and member index. + pub fn load(pdf_name: &str, member: usize) -> Self { + Self { + pdf: neopdf::pdf::PDF::load(pdf_name, member), + } + } +} + +impl PdfBackend for NeopdfPdf { + fn xfx_q2(&self, id: i32, x: f64, q2: f64) -> f64 { + self.pdf.xfxq2(id, &[x, q2]) + } + + fn alphas_q2(&self, q2: f64) -> f64 { + self.pdf.alphas_q2(q2) + } + + fn x_min(&mut self) -> f64 { + self.pdf.metadata().x_min + } + + fn x_max(&mut self) -> f64 { + self.pdf.metadata().x_max + } + + fn particle_id(&self) -> i32 { + self.pdf.metadata().hadron_pid + } + + fn is_polarized(&self) -> bool { + self.pdf.metadata().polarised + } + + fn set_type(&self) -> SetType { + match self.pdf.metadata().set_type { + neopdf::metadata::SetType::SpaceLike => SetType::SpaceLike, + neopdf::metadata::SetType::TimeLike => SetType::TimeLike, + } + } + + fn set_force_positive(&mut self, method: ForcePositive) { + match method { + ForcePositive::None => { + self.pdf + .set_force_positive(neopdf::gridpdf::ForcePositive::NoClipping); + } + ForcePositive::ClipNegative => { + self.pdf + .set_force_positive(neopdf::gridpdf::ForcePositive::ClipNegative); + } + } + } + + fn error_type(&self) -> String { + self.pdf.metadata().error_type.clone() + } +} + +/// `NeoPDF` set wrapper. +pub struct NeopdfSet { + pdf_name: String, + num_members: usize, + error_type: String, +} + +impl NeopdfSet { + /// Creates a new NeoPDF set by name. + pub fn new(pdf_name: &str) -> Self { + // Load member 0 to get metadata + let pdf0 = neopdf::pdf::PDF::load(pdf_name, 0); + let metadata = pdf0.metadata(); + + Self { + pdf_name: pdf_name.to_owned(), + num_members: metadata.num_members as usize, + error_type: metadata.error_type.clone(), + } + } +} + +impl PdfSetBackend for NeopdfSet { + fn num_members(&self) -> usize { + self.num_members + } + + fn mk_pdfs(&self) -> Result>> { + let pdfs = neopdf::pdf::PDF::load_pdfs(&self.pdf_name); + Ok(pdfs + .into_iter() + .map(|pdf| Box::new(NeopdfPdf { pdf }) as Box) + .collect()) + } + + fn uncertainty(&self, values: &[f64], cl: f64, alternative: bool) -> Result { + let unc = neopdf::uncertainty::uncertainty( + values, + &self.error_type, + neopdf::uncertainty::CL_1_SIGMA, + cl, + alternative, + ) + .map_err(|e| anyhow::anyhow!(e))?; + + Ok(Uncertainty { + central: unc.central, + errminus: unc.errminus, + errplus: unc.errplus, + }) + } +} + +// ============================================================================ +// Factory Functions +// ============================================================================ + +/// Creates a single PDF from a set name and member index. +pub fn create_pdf(name: &str, member: usize, backend: Backend) -> Result> { + match backend { + Backend::Lhapdf => { + if let Ok(lhaid) = name.parse::() { + Ok(Box::new(LhapdfPdf::with_lhaid(lhaid)?)) + } else { + Ok(Box::new(LhapdfPdf::with_setname_and_member( + name, + member.try_into().unwrap(), + )?)) + } + } + // NOTE: `NeoPDF` doesn't support reading LHAID yet. + Backend::Neopdf => Ok(Box::new(NeopdfPdf::load(name, member))), + } +} + +/// Creates a PDF set for uncertainty calculations. +pub fn create_pdf_set(name: &str, backend: Backend) -> Result> { + match backend { + Backend::Lhapdf => { + // Try parsing as LHAID first + let setname = if let Ok(lhaid) = name.parse::() { + lhapdf::lookup_pdf(lhaid) + .map(|(set, _)| set) + .ok_or_else(|| anyhow!("no PDF set found for LHAID = {lhaid}"))? + } else { + name.to_owned() + }; + Ok(Box::new(LhapdfSet::new(&setname)?)) + } + Backend::Neopdf => Ok(Box::new(NeopdfSet::new(name))), + } +} diff --git a/pineappl_cli/src/uncert.rs b/pineappl_cli/src/uncert.rs index 1a21127c..9f097508 100644 --- a/pineappl_cli/src/uncert.rs +++ b/pineappl_cli/src/uncert.rs @@ -66,7 +66,7 @@ pub struct Opts { #[command(flatten)] group: Group, /// Confidence level in per cent, for convolution function uncertainties. - #[arg(default_value_t = lhapdf::CL_1_SIGMA, long)] + #[arg(default_value_t = super::pdf_backend::CL_1_SIGMA, long)] cl: f64, /// Show integrated numbers (without bin widths) instead of differential ones. #[arg(long, short)] @@ -94,7 +94,8 @@ pub struct Opts { impl Subcommand for Opts { fn run(&self, cfg: &GlobalConfiguration) -> Result { let grid = helpers::read_grid(&self.input)?; - let mut conv_funs = helpers::create_conv_funs(&self.conv_funs)?; + let mut conv_funs = + helpers::create_conv_funs_with_backend(&self.conv_funs, cfg.pdf_backend)?; let limits = helpers::convolve_limits( &grid, @@ -116,11 +117,15 @@ impl Subcommand for Opts { .conv_fun .iter() .map(|&index| { - let (set, funs) = helpers::create_conv_funs_for_set(&self.conv_funs, index)?; + let (set, funs) = helpers::create_conv_funs_for_set_with_backend( + &self.conv_funs, + index, + cfg.pdf_backend, + )?; let results: Vec<_> = funs .into_par_iter() .map(|mut funs| { - Ok::<_, Error>(helpers::convolve( + Ok::<_, Error>(helpers::convolve_with_backend( &grid, &mut funs, &self.conv_funs.conv_types, @@ -159,7 +164,7 @@ impl Subcommand for Opts { .max() .unwrap_or(1); let scale_tuples = helpers::scales_vector(&grid, scales_max); - let scale_results = helpers::convolve_scales( + let scale_results = helpers::convolve_scales_with_backend( &grid, &mut conv_funs, &self.conv_funs.conv_types, diff --git a/pineappl_cli/tests/convolve.rs b/pineappl_cli/tests/convolve.rs index 338bada4..67a990c8 100644 --- a/pineappl_cli/tests/convolve.rs +++ b/pineappl_cli/tests/convolve.rs @@ -536,6 +536,86 @@ fn issue_334() { .stdout(NO_CHANNELS_GRID_STR); } +#[test] +fn neopdf_backend() { + Command::cargo_bin("pineappl") + .unwrap() + .args([ + "--pdf-backend=neopdf", + "convolve", + "../test-data/LHCB_WP_7TEV_opt.pineappl.lz4", + "NNPDF31_nlo_as_0118_luxqed", + ]) + .assert() + .success() + .stdout(str::contains(DEFAULT_STR)); +} + +#[test] +fn integrated_neopdf_backend() { + Command::cargo_bin("pineappl") + .unwrap() + .args([ + "--pdf-backend=neopdf", + "convolve", + "--integrated", + "../test-data/LHCB_WP_7TEV_opt.pineappl.lz4", + "NNPDF31_nlo_as_0118_luxqed", + ]) + .assert() + .success() + .stdout(INTEGRATED_STR); +} + +#[test] +fn three_pdfs_neopdf_backend() { + Command::cargo_bin("pineappl") + .unwrap() + .args([ + "--pdf-backend=neopdf", + "convolve", + "../test-data/LHCB_WP_7TEV_opt.pineappl.lz4", + "NNPDF31_nlo_as_0118_luxqed/0", + "NNPDF31_nlo_as_0118_luxqed/1", + "NNPDF31_nlo_as_0118_luxqed/2", + ]) + .assert() + .success() + .stdout(THREE_PDFS_STR); +} + +#[test] +fn bins_13567_neopdf_backend() { + Command::cargo_bin("pineappl") + .unwrap() + .args([ + "--pdf-backend=neopdf", + "convolve", + "--bins=1,3,5-7", + "../test-data/LHCB_WP_7TEV_opt.pineappl.lz4", + "NNPDF31_nlo_as_0118_luxqed", + ]) + .assert() + .success() + .stdout(BINS_13567_STR); +} + +#[test] +fn force_positive_neopdf_backend() { + Command::cargo_bin("pineappl") + .unwrap() + .args([ + "--force-positive", + "--pdf-backend=neopdf", + "convolve", + "../test-data/LHCB_WP_7TEV_opt.pineappl.lz4", + "NNPDF31_nlo_as_0118_luxqed", + ]) + .assert() + .success() + .stdout(FORCE_POSITIVE_STR); +} + #[test] fn lagrange_subgrid_v1() { Command::cargo_bin("pineappl") diff --git a/pineappl_cli/tests/main.rs b/pineappl_cli/tests/main.rs index 8f6b2e34..2074f377 100644 --- a/pineappl_cli/tests/main.rs +++ b/pineappl_cli/tests/main.rs @@ -29,6 +29,7 @@ Options: --force-positive Forces negative PDF values to zero --allow-extrapolation Allow extrapolation of PDFs outside their region of validity --use-alphas-from Choose the PDF/FF set for the strong coupling [default: 0] + --pdf-backend Select the PDF interpolation backend: 'lhapdf' or 'neopdf' [default: lhapdf] -h, --help Print help -V, --version Print version "; diff --git a/pineappl_cli/tests/uncert.rs b/pineappl_cli/tests/uncert.rs index b0bfcd63..6e1483ce 100644 --- a/pineappl_cli/tests/uncert.rs +++ b/pineappl_cli/tests/uncert.rs @@ -1,6 +1,7 @@ #![allow(missing_docs)] use assert_cmd::Command; +use predicates::str; use std::num::NonZeroUsize; use std::thread; @@ -204,6 +205,28 @@ const SCALE_ENV_9_STR: &str = "b etal dsig/detal 9pt-svar (env) 7 4 4.5 2.7517266e1 -5.36 5.22 "; +const NEOPDF_CONV_FUN_STR: &str = "-+----+----+-----------+-----------+---------+--------- +0 2 2.25 7.5459110e2 7.5461655e2 -1.14 1.14 +1 2.25 2.5 6.9028342e2 6.9027941e2 -1.16 1.16 +2 2.5 2.75 6.0025198e2 6.0022595e2 -1.18 1.18 +3 2.75 3 4.8552235e2 4.8548211e2 -1.22 1.22 +4 3 3.25 3.6195456e2 3.6191001e2 -1.27 1.27 +5 3.25 3.5 2.4586691e2 2.4582640e2 -1.35 1.35 +6 3.5 4 1.1586851e2 1.1584074e2 -1.51 1.51 +7 4 4.5 2.7517266e1 2.7504644e1 -2.77 2.77 +"; + +const NEOPDF_CONV_FUN_CL_90_STR: &str = "-+----+----+-----------+-----------+---------+--------- +0 2 2.25 7.5459110e2 7.5461655e2 -1.87 1.87 +1 2.25 2.5 6.9028342e2 6.9027941e2 -1.90 1.90 +2 2.5 2.75 6.0025198e2 6.0022595e2 -1.95 1.95 +3 2.75 3 4.8552235e2 4.8548211e2 -2.00 2.00 +4 3 3.25 3.6195456e2 3.6191001e2 -2.08 2.08 +5 3.25 3.5 2.4586691e2 2.4582640e2 -2.22 2.22 +6 3.5 4 1.1586851e2 1.1584074e2 -2.48 2.48 +7 4 4.5 2.7517266e1 2.7504644e1 -4.56 4.56 +"; + #[test] fn help() { Command::cargo_bin("pineappl") @@ -437,3 +460,88 @@ fn scale_env_9() { .success() .stdout(SCALE_ENV_9_STR); } + +#[test] +fn conv_fun_neopdf_backend() { + Command::cargo_bin("pineappl") + .unwrap() + .args([ + "--pdf-backend=neopdf", + "uncert", + "--conv-fun", + "--threads=1", + "../test-data/LHCB_WP_7TEV_opt.pineappl.lz4", + "NNPDF31_nlo_as_0118_luxqed", + ]) + .assert() + .success() + .stdout(str::contains(NEOPDF_CONV_FUN_STR)); +} + +#[test] +fn conv_fun_cl_90_neopdf_backend() { + Command::cargo_bin("pineappl") + .unwrap() + .args([ + "--pdf-backend=neopdf", + "uncert", + "--conv-fun", + "--cl=90", + "--threads=1", + "../test-data/LHCB_WP_7TEV_opt.pineappl.lz4", + "NNPDF31_nlo_as_0118_luxqed", + ]) + .assert() + .success() + .stdout(str::contains(NEOPDF_CONV_FUN_CL_90_STR)); +} + +#[test] +fn scale_env_neopdf_backend() { + Command::cargo_bin("pineappl") + .unwrap() + .args([ + "--pdf-backend=neopdf", + "uncert", + "--scale-env", + "../test-data/LHCB_WP_7TEV_opt.pineappl.lz4", + "NNPDF31_nlo_as_0118_luxqed", + ]) + .assert() + .success() + .stdout(str::contains(SCALE_ENV_STR)); +} + +#[test] +fn conv_fun_orders_a2_as1a2_neopdf_beckend() { + Command::cargo_bin("pineappl") + .unwrap() + .args([ + "--pdf-backend=neopdf", + "uncert", + "--conv-fun=0", + "--orders=a2,as1a2", + "--threads=1", + "../test-data/LHCB_WP_7TEV_opt.pineappl.lz4", + "NNPDF31_nlo_as_0118_luxqed", + ]) + .assert() + .success() + .stdout(ORDERS_A2_AS1A2_STR); +} + +#[test] +fn scale_abs_9_neopdf_backend() { + Command::cargo_bin("pineappl") + .unwrap() + .args([ + "--pdf-backend=neopdf", + "uncert", + "--scale-abs=9", + "../test-data/LHCB_WP_7TEV_opt.pineappl.lz4", + "NNPDF31_nlo_as_0118_luxqed", + ]) + .assert() + .success() + .stdout(SCALE_ABS_9_STR); +}