managed_lhapdf/
manager.rs

1//! Managing functions. These functions wrap the functions from LHAPDF that mail fail due to data
2//! not being downloaded. In that case we do the best to download them from locations and to a
3//! directory specified in our configuration file.
4
5use super::ffi::{self, PDFSet, PDF};
6use super::unmanaged;
7use super::{Error, Result};
8use cxx::UniquePtr;
9use flate2::read::GzDecoder;
10use fs2::FileExt;
11use serde::{Deserialize, Serialize};
12use std::env;
13use std::fs::{self, File};
14use std::io;
15use std::io::{ErrorKind, Write};
16use std::path::Path;
17use std::sync::{Mutex, OnceLock};
18use tar::Archive;
19
20const LHAPDF_CONFIG: &str = "Verbosity: 1
21Interpolator: logcubic
22Extrapolator: continuation
23ForcePositive: 0
24AlphaS_Type: analytic
25MZ: 91.1876
26MUp: 0.002
27MDown: 0.005
28MStrange: 0.10
29MCharm: 1.29
30MBottom: 4.19
31MTop: 172.9
32Pythia6LambdaV5Compat: true
33";
34
35/// Configuration for this library.
36#[derive(Debug, Deserialize, Serialize)]
37#[serde(deny_unknown_fields)]
38pub struct Config {
39    lhapdf_data_path_read: Vec<String>,
40    lhapdf_data_path_write: String,
41    pdfsets_index_url: String,
42    pdfset_urls: Vec<String>,
43}
44
45struct LhapdfData;
46
47impl Config {
48    /// Return the only instance of this type.
49    pub fn get() -> &'static Self {
50        static SINGLETON: OnceLock<Result<Config>> = OnceLock::new();
51
52        let config = SINGLETON.get_or_init(|| {
53            let config_path = dirs::config_dir()
54                .ok_or_else(|| Error::General("no configuration directory found".to_owned()))?;
55
56            // create the configuration directory if it doesn't exist yet - in practice this only
57            // happens in our CI
58            fs::create_dir_all(&config_path)?;
59
60            let config_path = config_path.join("managed-lhapdf.toml");
61
62            // TODO: it's possible that multiple processes try to create the default configuration
63            // file and/or that while the file is created, other processes try to read from it
64
65            // MSRV 1.77.0: use `File::create_new` instead
66            let config = match File::options()
67                .read(true)
68                .write(true)
69                .create_new(true)
70                .open(&config_path)
71            {
72                // the file didn't exist before
73                Ok(mut file) => {
74                    // use a default configuration
75                    let mut config = Self {
76                        lhapdf_data_path_read: vec![],
77                        lhapdf_data_path_write: dirs::data_dir()
78                            .ok_or_else(|| Error::General("no data directory found".to_owned()))?
79                            .join("managed-lhapdf")
80                            .to_str()
81                            // UNWRAP: if the string isn't valid unicode we can't proceed
82                            .unwrap()
83                            .to_owned(),
84                        pdfsets_index_url: "https://lhapdfsets.web.cern.ch/current/pdfsets.index"
85                            .to_owned(),
86                        pdfset_urls: vec!["https://lhapdfsets.web.cern.ch/current/".to_owned()],
87                    };
88
89                    // if there's an environment variable that the user set use its value
90                    if let Some(os_str) =
91                        env::var_os("LHAPDF_DATA_PATH").or_else(|| env::var_os("LHAPATH"))
92                    {
93                        config.lhapdf_data_path_read =
94                            // UNWRAP: if the string isn't valid unicode we can't proceed
95                            os_str.to_str().unwrap().split(':').map(ToOwned::to_owned).collect();
96                    }
97
98                    file.write_all(toml::to_string_pretty(&config)?.as_bytes())?;
99
100                    config
101                }
102                Err(err) if err.kind() == ErrorKind::AlreadyExists => {
103                    // the file already exists, simply read it
104                    toml::from_str(&fs::read_to_string(&config_path)?)?
105                }
106                Err(err) => Err(err)?,
107            };
108
109            if let Some(lhapdf_data_path_write) = config.lhapdf_data_path_write() {
110                // create download directory for `lhapdf.conf`
111                fs::create_dir_all(lhapdf_data_path_write)?;
112
113                // MSRV 1.77.0: use `File::create_new` instead
114                if let Ok(mut file) = File::options()
115                    .read(true)
116                    .write(true)
117                    .create_new(true)
118                    .open(Path::new(lhapdf_data_path_write).join("lhapdf.conf"))
119                {
120                    // if `lhapdf.conf` doesn't exist, create it
121                    file.write_all(LHAPDF_CONFIG.as_bytes())?;
122                }
123
124                let pdfsets_index = Path::new(lhapdf_data_path_write).join("pdfsets.index");
125
126                // MSRV 1.77.0: use `File::create_new` instead
127                if let Ok(mut file) = File::options()
128                    .read(true)
129                    .write(true)
130                    .create_new(true)
131                    .open(pdfsets_index)
132                {
133                    // if `pdfsets.index` doesn't exist, download it
134                    let mut reader = ureq::get(config.pdfsets_index_url()).call()?.into_reader();
135                    io::copy(&mut reader, &mut file)?;
136                }
137            }
138
139            // we use the environment variable `LHAPDF_DATA_PATH` to let LHAPDF know where we've
140            // stored our PDFs
141
142            let mut lhapdf_data_path = config
143                .lhapdf_data_path_write()
144                .map_or_else(Vec::new, |path| vec![path.to_owned()]);
145            lhapdf_data_path.extend(config.lhapdf_data_path_read.iter().cloned());
146            // as long as `static Config _cfg` in LHAPDF's `src/Config.cc` is `static` and not
147            // `thread_local`, this belongs here; otherwise move it out of the singleton
148            // initialization
149            env::set_var("LHAPDF_DATA_PATH", lhapdf_data_path.join(":"));
150
151            Ok(config)
152        });
153
154        // TODO: change return type and propagate the result - difficult because we can't clone the
155        // error type
156        config.as_ref().unwrap()
157    }
158
159    /// Return the path where `managed-lhapdf` will download PDF sets and `pdfsets.index` to.
160    pub fn lhapdf_data_path_write(&self) -> Option<&str> {
161        if self.lhapdf_data_path_write.is_empty() {
162            None
163        } else {
164            Some(&self.lhapdf_data_path_write)
165        }
166    }
167
168    /// Return the URL where the file `pdfsets.index` will downloaded from.
169    pub fn pdfsets_index_url(&self) -> &str {
170        &self.pdfsets_index_url
171    }
172
173    /// Return the URLs that should be searched for PDF sets, if they are not available in the
174    /// local cache.
175    pub fn pdfset_urls(&self) -> &[String] {
176        &self.pdfset_urls
177    }
178}
179
180impl From<toml::ser::Error> for Error {
181    fn from(err: toml::ser::Error) -> Self {
182        Self::Other(anyhow::Error::new(err))
183    }
184}
185
186impl From<toml::de::Error> for Error {
187    fn from(err: toml::de::Error) -> Self {
188        Self::Other(anyhow::Error::new(err))
189    }
190}
191
192impl From<ureq::Error> for Error {
193    fn from(err: ureq::Error) -> Self {
194        Self::Other(anyhow::Error::new(err))
195    }
196}
197
198impl LhapdfData {
199    fn get() -> &'static Mutex<Self> {
200        static SINGLETON: Mutex<LhapdfData> = Mutex::new(LhapdfData);
201        &SINGLETON
202    }
203
204    fn download_set(&self, name: &str, config: &Config) -> Result<()> {
205        if let Some(lhapdf_data_path_write) = config.lhapdf_data_path_write() {
206            let lock_file =
207                File::create(Path::new(lhapdf_data_path_write).join(format!("{name}.lock")))?;
208            lock_file.lock_exclusive()?;
209
210            for url in config.pdfset_urls() {
211                let response = ureq::get(&format!("{url}/{name}.tar.gz")).call();
212
213                if let Err(ureq::Error::Status(404, _)) = response {
214                    continue;
215                }
216
217                let reader = response?.into_reader();
218
219                // TODO: what if multiple threads/processes try to write to the same file?
220                Archive::new(GzDecoder::new(reader)).unpack(lhapdf_data_path_write)?;
221
222                // we found a PDF set, now it's LHAPDF's turn
223                break;
224            }
225
226            lock_file.unlock()?;
227        }
228
229        Ok(())
230    }
231
232    fn update_pdfsets_index(&self, config: &Config) -> Result<()> {
233        if let Some(lhapdf_data_path_write) = config.lhapdf_data_path_write() {
234            let lock_file = File::create(Path::new(lhapdf_data_path_write).join("pdfsets.lock"))?;
235            lock_file.lock_exclusive()?;
236
237            // empty the `static thread_local` variable sitting in `getPDFIndex` to trigger the
238            // re-initialization of this variable
239            ffi::empty_lhaindex();
240
241            // download `pdfsets.index`
242            let content = ureq::get(config.pdfsets_index_url())
243                .call()?
244                .into_string()?;
245
246            let pdfsets_index = Path::new(lhapdf_data_path_write).join("pdfsets.index");
247
248            // TODO: what if multiple threads/processes try to write to the same file?
249            File::create(pdfsets_index)?.write_all(content.as_bytes())?;
250
251            lock_file.unlock()?;
252        }
253
254        Ok(())
255    }
256
257    pub fn pdf_name_and_member_via_lhaid(&self, lhaid: i32) -> Option<(String, i32)> {
258        unmanaged::pdf_name_and_member_via_lhaid(lhaid)
259    }
260
261    fn pdf_with_setname_and_member(&self, setname: &str, member: i32) -> Result<UniquePtr<PDF>> {
262        unmanaged::pdf_with_setname_and_member(setname, member)
263    }
264
265    fn pdfset_new(&self, setname: &str) -> Result<UniquePtr<PDFSet>> {
266        unmanaged::pdfset_new(setname)
267    }
268
269    fn set_verbosity(&self, verbosity: i32) {
270        unmanaged::set_verbosity(verbosity);
271    }
272
273    fn verbosity(&self) -> i32 {
274        unmanaged::verbosity()
275    }
276}
277
278pub fn pdf_name_and_member_via_lhaid(lhaid: i32) -> Option<(String, i32)> {
279    // this must be the first call before anything from LHAPDF
280    let config = Config::get();
281
282    // TODO: change return type of this function and handle the error properly
283    let lock = LhapdfData::get().lock().unwrap();
284
285    lock.pdf_name_and_member_via_lhaid(lhaid).or_else(|| {
286        // TODO: change return type of this function and handle the error properly
287        lock.update_pdfsets_index(config).unwrap();
288        lock.pdf_name_and_member_via_lhaid(lhaid)
289    })
290}
291
292pub fn pdf_with_setname_and_member(setname: &str, member: i32) -> Result<UniquePtr<PDF>> {
293    // this must be the first call before anything from LHAPDF
294    let config = Config::get();
295
296    // TODO: handle error properly
297    let lock = LhapdfData::get().lock().unwrap();
298
299    lock.pdf_with_setname_and_member(setname, member)
300        .or_else(|err: Error| {
301            // here we rely on exactly matching LHAPDF's exception string
302            if err.to_string() == format!("Info file not found for PDF set '{setname}'") {
303                lock.download_set(setname, config)
304                    .and_then(|()| lock.pdf_with_setname_and_member(setname, member))
305            } else {
306                Err(err)
307            }
308        })
309}
310
311pub fn pdfset_new(setname: &str) -> Result<UniquePtr<PDFSet>> {
312    // this must be the first call before anything from LHAPDF
313    let config = Config::get();
314
315    // TODO: handle error properly
316    let lock = LhapdfData::get().lock().unwrap();
317
318    lock.pdfset_new(setname).or_else(|err: Error| {
319        // here we rely on exactly matching LHAPDF's exception string
320        if err.to_string() == format!("Info file not found for PDF set '{setname}'") {
321            lock.download_set(setname, config)
322                .and_then(|()| lock.pdfset_new(setname))
323        } else {
324            Err(err)
325        }
326    })
327}
328
329pub fn set_verbosity(verbosity: i32) {
330    // this must be the first call before anything from LHAPDF
331    let _ = Config::get();
332
333    // TODO: handle error properly
334    let lock = LhapdfData::get().lock().unwrap();
335
336    lock.set_verbosity(verbosity);
337}
338
339pub fn verbosity() -> i32 {
340    // this must be the first call before anything from LHAPDF
341    let _ = Config::get();
342
343    // TODO: handle error properly
344    let lock = LhapdfData::get().lock().unwrap();
345
346    lock.verbosity()
347}