laddu_extensions/
likelihoods.rs

1use std::{
2    collections::HashMap,
3    fmt::{Debug, Display},
4    sync::Arc,
5};
6
7use crate::RngSubsetExtension;
8use accurate::{sum::Klein, traits::*};
9use auto_ops::*;
10use dyn_clone::DynClone;
11use fastrand::Rng;
12use laddu_core::{
13    amplitudes::{central_difference, AmplitudeValues, Evaluator, GradientValues, Model},
14    data::Dataset,
15    resources::Parameters,
16    Float, LadduError,
17};
18use nalgebra::DVector;
19use num::Complex;
20
21#[cfg(feature = "mpi")]
22use laddu_core::mpi::LadduMPI;
23
24#[cfg(feature = "mpi")]
25use mpi::{datatype::PartitionMut, topology::SimpleCommunicator, traits::*};
26use parking_lot::Mutex;
27
28#[cfg(feature = "python")]
29use crate::ganesh_ext::{
30    py_ganesh::{FromPyArgs, PyMCMCSummary, PyMinimizationSummary},
31    MCMCSettings, MinimizationSettings,
32};
33#[cfg(feature = "python")]
34use laddu_python::{
35    amplitudes::{PyEvaluator, PyModel},
36    data::PyDataset,
37};
38#[cfg(feature = "python")]
39use numpy::PyArray1;
40#[cfg(feature = "python")]
41use pyo3::{
42    exceptions::PyTypeError,
43    prelude::*,
44    types::{PyDict, PyList},
45};
46#[cfg(feature = "rayon")]
47use rayon::prelude::*;
48#[cfg(all(feature = "python", feature = "rayon"))]
49use rayon::ThreadPoolBuilder;
50
51/// A trait which describes a term that can be used like a likelihood (more correctly, a negative
52/// log-likelihood) in a minimization.
53pub trait LikelihoodTerm: DynClone + Send + Sync {
54    /// Evaluate the term given some input parameters.
55    fn evaluate(&self, parameters: &[Float]) -> Float;
56    /// Evaluate the gradient of the term given some input parameters.
57    fn evaluate_gradient(&self, parameters: &[Float]) -> DVector<Float> {
58        central_difference(parameters, |parameters: &[Float]| self.evaluate(parameters))
59    }
60    /// The list of names of the input parameters for [`LikelihoodTerm::evaluate`].
61    fn parameters(&self) -> Vec<String>;
62    /// A method called every step of any minimization/MCMC algorithm.
63    fn update(&self) {}
64}
65
66dyn_clone::clone_trait_object!(LikelihoodTerm);
67
68/// A term in an expression with multiple likelihood components
69///
70/// See Also
71/// --------
72/// NLL.to_term
73///
74#[cfg(feature = "python")]
75#[pyclass(name = "LikelihoodTerm", module = "laddu")]
76#[derive(Clone)]
77pub struct PyLikelihoodTerm(pub Box<dyn LikelihoodTerm>);
78
79/// An extended, unbinned negative log-likelihood evaluator.
80#[derive(Clone)]
81pub struct NLL {
82    /// The internal [`Evaluator`] for data
83    pub data_evaluator: Evaluator,
84    /// The internal [`Evaluator`] for accepted Monte Carlo
85    pub accmc_evaluator: Evaluator,
86}
87
88impl NLL {
89    /// Construct an [`NLL`] from a [`Model`] and two [`Dataset`]s (data and Monte Carlo). This is the equivalent of the [`Model::load`] method,
90    /// but for two [`Dataset`]s and a different method of evaluation.
91    pub fn new(model: &Model, ds_data: &Arc<Dataset>, ds_accmc: &Arc<Dataset>) -> Box<Self> {
92        Self {
93            data_evaluator: model.load(ds_data),
94            accmc_evaluator: model.load(ds_accmc),
95        }
96        .into()
97    }
98    /// Create a new [`StochasticNLL`] from this [`NLL`].
99    pub fn to_stochastic(&self, batch_size: usize, seed: Option<usize>) -> StochasticNLL {
100        StochasticNLL::new(self.clone(), batch_size, seed)
101    }
102    /// Activate an [`Amplitude`](`laddu_core::amplitudes::Amplitude`) by name.
103    pub fn activate<T: AsRef<str>>(&self, name: T) -> Result<(), LadduError> {
104        self.data_evaluator.activate(&name)?;
105        self.accmc_evaluator.activate(&name)
106    }
107    /// Activate several [`Amplitude`](`laddu_core::amplitudes::Amplitude`)s by name.
108    pub fn activate_many<T: AsRef<str>>(&self, names: &[T]) -> Result<(), LadduError> {
109        self.data_evaluator.activate_many(names)?;
110        self.accmc_evaluator.activate_many(names)
111    }
112    /// Activate all registered [`Amplitude`](`laddu_core::amplitudes::Amplitude`)s.
113    pub fn activate_all(&self) {
114        self.data_evaluator.activate_all();
115        self.accmc_evaluator.activate_all();
116    }
117    /// Dectivate an [`Amplitude`](`laddu_core::amplitudes::Amplitude`) by name.
118    pub fn deactivate<T: AsRef<str>>(&self, name: T) -> Result<(), LadduError> {
119        self.data_evaluator.deactivate(&name)?;
120        self.accmc_evaluator.deactivate(&name)
121    }
122    /// Deactivate several [`Amplitude`](`laddu_core::amplitudes::Amplitude`)s by name.
123    pub fn deactivate_many<T: AsRef<str>>(&self, names: &[T]) -> Result<(), LadduError> {
124        self.data_evaluator.deactivate_many(names)?;
125        self.accmc_evaluator.deactivate_many(names)
126    }
127    /// Deactivate all registered [`Amplitude`](`laddu_core::amplitudes::Amplitude`)s.
128    pub fn deactivate_all(&self) {
129        self.data_evaluator.deactivate_all();
130        self.accmc_evaluator.deactivate_all();
131    }
132    /// Isolate an [`Amplitude`](`laddu_core::amplitudes::Amplitude`) by name (deactivate the rest).
133    pub fn isolate<T: AsRef<str>>(&self, name: T) -> Result<(), LadduError> {
134        self.data_evaluator.isolate(&name)?;
135        self.accmc_evaluator.isolate(&name)
136    }
137    /// Isolate several [`Amplitude`](`laddu_core::amplitudes::Amplitude`)s by name (deactivate the rest).
138    pub fn isolate_many<T: AsRef<str>>(&self, names: &[T]) -> Result<(), LadduError> {
139        self.data_evaluator.isolate_many(names)?;
140        self.accmc_evaluator.isolate_many(names)
141    }
142
143    /// Project the stored [`Model`] over the events in the [`Dataset`] stored by the
144    /// [`Evaluator`] with the given values for free parameters to obtain weights for each
145    /// Monte-Carlo event (non-MPI version).
146    ///
147    /// # Notes
148    ///
149    /// This method is not intended to be called in analyses but rather in writing methods
150    /// that have `mpi`-feature-gated versions. Most users will want to call [`NLL::project`] instead.
151    pub fn project_local(
152        &self,
153        parameters: &[Float],
154        mc_evaluator: Option<Evaluator>,
155    ) -> Vec<Float> {
156        let (mc_dataset, result) = if let Some(mc_evaluator) = mc_evaluator {
157            (
158                mc_evaluator.dataset.clone(),
159                mc_evaluator.evaluate_local(parameters),
160            )
161        } else {
162            (
163                self.accmc_evaluator.dataset.clone(),
164                self.accmc_evaluator.evaluate_local(parameters),
165            )
166        };
167        let n_mc = self.accmc_evaluator.dataset.n_events_weighted();
168        #[cfg(feature = "rayon")]
169        let output: Vec<Float> = result
170            .par_iter()
171            .zip(mc_dataset.events.par_iter())
172            .map(|(l, e)| e.weight * l.re / n_mc)
173            .collect();
174
175        #[cfg(not(feature = "rayon"))]
176        let output: Vec<Float> = result
177            .iter()
178            .zip(mc_dataset.events.iter())
179            .map(|(l, e)| e.weight * l.re / n_mc)
180            .collect();
181        output
182    }
183
184    /// Project the stored [`Model`] over the events in the [`Dataset`] stored by the
185    /// [`Evaluator`] with the given values for free parameters to obtain weights for each
186    /// Monte-Carlo event (MPI-compatible version).
187    ///
188    /// # Notes
189    ///
190    /// This method is not intended to be called in analyses but rather in writing methods
191    /// that have `mpi`-feature-gated versions. Most users will want to call [`NLL::project`] instead.
192    #[cfg(feature = "mpi")]
193    pub fn project_mpi(
194        &self,
195        parameters: &[Float],
196        mc_evaluator: Option<Evaluator>,
197        world: &SimpleCommunicator,
198    ) -> Vec<Float> {
199        let n_events = mc_evaluator
200            .as_ref()
201            .unwrap_or(&self.accmc_evaluator)
202            .dataset
203            .n_events();
204        let local_projection = self.project_local(parameters, mc_evaluator);
205        let mut buffer: Vec<Float> = vec![0.0; n_events];
206        let (counts, displs) = world.get_counts_displs(n_events);
207        {
208            let mut partitioned_buffer = PartitionMut::new(&mut buffer, counts, displs);
209            world.all_gather_varcount_into(&local_projection, &mut partitioned_buffer);
210        }
211        buffer
212    }
213
214    /// Project the stored [`Model`] over the events in the [`Dataset`] stored by the
215    /// [`Evaluator`] with the given values for free parameters to obtain weights for each
216    /// Monte-Carlo event. This method takes the real part of the given expression (discarding
217    /// the imaginary part entirely, which does not matter if expressions are coherent sums
218    /// wrapped in [`Expression::norm_sqr`](`laddu_core::Expression::norm_sqr`).
219    /// Event weights are determined by the following formula:
220    ///
221    /// ```math
222    /// \text{weight}(\vec{p}; e) = \text{weight}(e) \mathcal{L}(e) / N_{\text{MC}}
223    /// ```
224    ///
225    /// Note that $`N_{\text{MC}}`$ will always be the number of accepted Monte Carlo events,
226    /// regardless of the `mc_evaluator`.
227    pub fn project(&self, parameters: &[Float], mc_evaluator: Option<Evaluator>) -> Vec<Float> {
228        #[cfg(feature = "mpi")]
229        {
230            if let Some(world) = laddu_core::mpi::get_world() {
231                return self.project_mpi(parameters, mc_evaluator, &world);
232            }
233        }
234        self.project_local(parameters, mc_evaluator)
235    }
236
237    /// Project the stored [`Model`] over the events in the [`Dataset`] stored by the
238    /// [`Evaluator`] with the given values for free parameters to obtain weights and gradients of
239    /// those weights for each Monte-Carlo event (non-MPI version).
240    ///
241    /// # Notes
242    ///
243    /// This method is not intended to be called in analyses but rather in writing methods
244    /// that have `mpi`-feature-gated versions. Most users will want to call [`NLL::project_gradient`] instead.
245    pub fn project_gradient_local(
246        &self,
247        parameters: &[Float],
248        mc_evaluator: Option<Evaluator>,
249    ) -> (Vec<Float>, Vec<DVector<Float>>) {
250        let (mc_dataset, result, result_gradient) = if let Some(mc_evaluator) = mc_evaluator {
251            (
252                mc_evaluator.dataset.clone(),
253                mc_evaluator.evaluate_local(parameters),
254                mc_evaluator.evaluate_gradient_local(parameters),
255            )
256        } else {
257            (
258                self.accmc_evaluator.dataset.clone(),
259                self.accmc_evaluator.evaluate_local(parameters),
260                self.accmc_evaluator.evaluate_gradient_local(parameters),
261            )
262        };
263        let n_mc = self.accmc_evaluator.dataset.n_events_weighted();
264        #[cfg(feature = "rayon")]
265        {
266            (
267                result
268                    .par_iter()
269                    .zip(mc_dataset.events.par_iter())
270                    .map(|(l, e)| e.weight * l.re / n_mc)
271                    .collect(),
272                result_gradient
273                    .par_iter()
274                    .zip(mc_dataset.events.par_iter())
275                    .map(|(grad_l, e)| grad_l.map(|g| g.re).scale(e.weight / n_mc))
276                    .collect(),
277            )
278        }
279        #[cfg(not(feature = "rayon"))]
280        {
281            (
282                result
283                    .iter()
284                    .zip(mc_dataset.events.iter())
285                    .map(|(l, e)| e.weight * l.re / n_mc)
286                    .collect(),
287                result_gradient
288                    .iter()
289                    .zip(mc_dataset.events.iter())
290                    .map(|(grad_l, e)| grad_l.map(|g| g.re).scale(e.weight / n_mc))
291                    .collect(),
292            )
293        }
294    }
295
296    /// Project the stored [`Model`] over the events in the [`Dataset`] stored by the
297    /// [`Evaluator`] with the given values for free parameters to obtain weights and gradients of
298    /// those weights for each Monte-Carlo event (MPI-compatible version).
299    ///
300    /// # Notes
301    ///
302    /// This method is not intended to be called in analyses but rather in writing methods
303    /// that have `mpi`-feature-gated versions. Most users will want to call [`NLL::project_gradient`] instead.
304    #[cfg(feature = "mpi")]
305    pub fn project_gradient_mpi(
306        &self,
307        parameters: &[Float],
308        mc_evaluator: Option<Evaluator>,
309        world: &SimpleCommunicator,
310    ) -> (Vec<Float>, Vec<DVector<Float>>) {
311        let n_events = mc_evaluator
312            .as_ref()
313            .unwrap_or(&self.accmc_evaluator)
314            .dataset
315            .n_events();
316        let (local_projection, local_gradient_projection) =
317            self.project_gradient_local(parameters, mc_evaluator);
318        let mut projection_result: Vec<Float> = vec![0.0; n_events];
319        let (counts, displs) = world.get_counts_displs(n_events);
320        {
321            let mut partitioned_buffer = PartitionMut::new(&mut projection_result, counts, displs);
322            world.all_gather_varcount_into(&local_projection, &mut partitioned_buffer);
323        }
324
325        let flattened_local_gradient_projection = local_gradient_projection
326            .iter()
327            .flat_map(|g| g.data.as_vec().to_vec())
328            .collect::<Vec<Float>>();
329        let (counts, displs) = world.get_flattened_counts_displs(n_events, parameters.len());
330        let mut flattened_result_buffer = vec![0.0; n_events * parameters.len()];
331        let mut partitioned_flattened_result_buffer =
332            PartitionMut::new(&mut flattened_result_buffer, counts, displs);
333        world.all_gather_varcount_into(
334            &flattened_local_gradient_projection,
335            &mut partitioned_flattened_result_buffer,
336        );
337        let gradient_projection_result = flattened_result_buffer
338            .chunks(parameters.len())
339            .map(DVector::from_row_slice)
340            .collect();
341        (projection_result, gradient_projection_result)
342    }
343    /// Project the stored [`Model`] over the events in the [`Dataset`] stored by the
344    /// [`Evaluator`] with the given values for free parameters to obtain weights and gradients of
345    /// those weights for each Monte-Carlo event. This method takes the real part of the given
346    /// expression (discarding the imaginary part entirely, which does not matter if expressions
347    /// are coherent sums wrapped in [`Expression::norm_sqr`](`laddu_core::Expression::norm_sqr`).
348    /// Event weights are determined by the following formula:
349    ///
350    /// ```math
351    /// \text{weight}(\vec{p}; e) = \text{weight}(e) \mathcal{L}(e) / N_{\text{MC}}
352    /// ```
353    ///
354    /// Note that $`N_{\text{MC}}`$ will always be the number of accepted Monte Carlo events,
355    /// regardless of the `mc_evaluator`.
356    pub fn project_gradient(
357        &self,
358        parameters: &[Float],
359        mc_evaluator: Option<Evaluator>,
360    ) -> (Vec<Float>, Vec<DVector<Float>>) {
361        #[cfg(feature = "mpi")]
362        {
363            if let Some(world) = laddu_core::mpi::get_world() {
364                return self.project_gradient_mpi(parameters, mc_evaluator, &world);
365            }
366        }
367        self.project_gradient_local(parameters, mc_evaluator)
368    }
369
370    /// Project the stored [`Model`] over the events in the [`Dataset`] stored by the
371    /// [`Evaluator`] with the given values for free parameters to obtain weights for each Monte-Carlo event. This method differs from the standard
372    /// [`NLL::project`] in that it first isolates the selected [`Amplitude`](`laddu_core::amplitudes::Amplitude`)s
373    /// by name, but returns the [`NLL`] to its prior state after calculation (non-MPI version).
374    ///
375    /// # Notes
376    ///
377    /// This method is not intended to be called in analyses but rather in writing methods
378    /// that have `mpi`-feature-gated versions. Most users will want to call [`NLL::project_with`] instead.
379    pub fn project_with_local<T: AsRef<str>>(
380        &self,
381        parameters: &[Float],
382        names: &[T],
383        mc_evaluator: Option<Evaluator>,
384    ) -> Result<Vec<Float>, LadduError> {
385        if let Some(mc_evaluator) = &mc_evaluator {
386            let current_active_mc = mc_evaluator.resources.read().active.clone();
387            mc_evaluator.isolate_many(names)?;
388            let mc_dataset = mc_evaluator.dataset.clone();
389            let result = mc_evaluator.evaluate_local(parameters);
390            let n_mc = self.accmc_evaluator.dataset.n_events_weighted();
391            #[cfg(feature = "rayon")]
392            let output: Vec<Float> = result
393                .par_iter()
394                .zip(mc_dataset.events.par_iter())
395                .map(|(l, e)| e.weight * l.re / n_mc)
396                .collect();
397            #[cfg(not(feature = "rayon"))]
398            let output: Vec<Float> = result
399                .iter()
400                .zip(mc_dataset.events.iter())
401                .map(|(l, e)| e.weight * l.re / n_mc)
402                .collect();
403            mc_evaluator.resources.write().active = current_active_mc;
404            Ok(output)
405        } else {
406            let current_active_data = self.data_evaluator.resources.read().active.clone();
407            let current_active_accmc = self.accmc_evaluator.resources.read().active.clone();
408            self.isolate_many(names)?;
409            let mc_dataset = &self.accmc_evaluator.dataset;
410            let result = self.accmc_evaluator.evaluate_local(parameters);
411            let n_mc = self.accmc_evaluator.dataset.n_events_weighted();
412            #[cfg(feature = "rayon")]
413            let output: Vec<Float> = result
414                .par_iter()
415                .zip(mc_dataset.events.par_iter())
416                .map(|(l, e)| e.weight * l.re / n_mc)
417                .collect();
418            #[cfg(not(feature = "rayon"))]
419            let output: Vec<Float> = result
420                .iter()
421                .zip(mc_dataset.events.iter())
422                .map(|(l, e)| e.weight * l.re / n_mc)
423                .collect();
424            self.data_evaluator.resources.write().active = current_active_data;
425            self.accmc_evaluator.resources.write().active = current_active_accmc;
426            Ok(output)
427        }
428    }
429
430    /// Project the stored [`Model`] over the events in the [`Dataset`] stored by the
431    /// [`Evaluator`] with the given values for free parameters to obtain weights for each Monte-Carlo event. This method differs from the standard
432    /// [`NLL::project`] in that it first isolates the selected [`Amplitude`](`laddu_core::amplitudes::Amplitude`)s
433    /// by name, but returns the [`NLL`] to its prior state after calculation (MPI-compatible version).
434    ///
435    /// # Notes
436    ///
437    /// This method is not intended to be called in analyses but rather in writing methods
438    /// that have `mpi`-feature-gated versions. Most users will want to call [`NLL::project_with`] instead.
439    #[cfg(feature = "mpi")]
440    pub fn project_with_mpi<T: AsRef<str>>(
441        &self,
442        parameters: &[Float],
443        names: &[T],
444        mc_evaluator: Option<Evaluator>,
445        world: &SimpleCommunicator,
446    ) -> Result<Vec<Float>, LadduError> {
447        let n_events = mc_evaluator
448            .as_ref()
449            .unwrap_or(&self.accmc_evaluator)
450            .dataset
451            .n_events();
452        let local_projection = self.project_with_local(parameters, names, mc_evaluator)?;
453        let mut buffer: Vec<Float> = vec![0.0; n_events];
454        let (counts, displs) = world.get_counts_displs(n_events);
455        {
456            let mut partitioned_buffer = PartitionMut::new(&mut buffer, counts, displs);
457            world.all_gather_varcount_into(&local_projection, &mut partitioned_buffer);
458        }
459        Ok(buffer)
460    }
461
462    /// Project the stored [`Model`] over the events in the [`Dataset`] stored by the
463    /// [`Evaluator`] with the given values for free parameters to obtain weights for each Monte-Carlo event. This method differs from the standard
464    /// [`NLL::project`] in that it first isolates the selected [`Amplitude`](`laddu_core::amplitudes::Amplitude`)s
465    /// by name, but returns the [`NLL`] to its prior state after calculation.
466    ///
467    /// This method takes the real part of the given expression (discarding
468    /// the imaginary part entirely, which does not matter if expressions are coherent sums
469    /// wrapped in [`Expression::norm_sqr`](`laddu_core::Expression::norm_sqr`).
470    /// Event weights are determined by the following formula:
471    ///
472    /// ```math
473    /// \text{weight}(\vec{p}; e) = \text{weight}(e) \mathcal{L}(e) / N_{\text{MC}}
474    /// ```
475    ///
476    /// Note that $`N_{\text{MC}}`$ will always be the number of accepted Monte Carlo events,
477    /// regardless of the `mc_evaluator`.
478    pub fn project_with<T: AsRef<str>>(
479        &self,
480        parameters: &[Float],
481        names: &[T],
482        mc_evaluator: Option<Evaluator>,
483    ) -> Result<Vec<Float>, LadduError> {
484        #[cfg(feature = "mpi")]
485        {
486            if let Some(world) = laddu_core::mpi::get_world() {
487                return self.project_with_mpi(parameters, names, mc_evaluator, &world);
488            }
489        }
490        self.project_with_local(parameters, names, mc_evaluator)
491    }
492
493    /// Project the stored [`Model`] over the events in the [`Dataset`] stored by the
494    /// [`Evaluator`] with the given values for free parameters to obtain weights and gradients of
495    /// those weights for each Monte-Carlo event. This method differs from the standard
496    /// [`NLL::project_gradient`] in that it first isolates the selected [`Amplitude`](`laddu_core::amplitudes::Amplitude`)s
497    /// by name, but returns the [`NLL`] to its prior state after calculation (non-MPI version).
498    ///
499    /// # Notes
500    ///
501    /// This method is not intended to be called in analyses but rather in writing methods
502    /// that have `mpi`-feature-gated versions. Most users will want to call [`NLL::project_with`] instead.
503    pub fn project_gradient_with_local<T: AsRef<str>>(
504        &self,
505        parameters: &[Float],
506        names: &[T],
507        mc_evaluator: Option<Evaluator>,
508    ) -> Result<(Vec<Float>, Vec<DVector<Float>>), LadduError> {
509        if let Some(mc_evaluator) = &mc_evaluator {
510            let current_active_mc = mc_evaluator.resources.read().active.clone();
511            mc_evaluator.isolate_many(names)?;
512            let mc_dataset = mc_evaluator.dataset.clone();
513            let result = mc_evaluator.evaluate_local(parameters);
514            let result_gradient = mc_evaluator.evaluate_gradient(parameters);
515            let n_mc = self.accmc_evaluator.dataset.n_events_weighted();
516            #[cfg(feature = "rayon")]
517            let (res, res_gradient) = {
518                (
519                    result
520                        .par_iter()
521                        .zip(mc_dataset.events.par_iter())
522                        .map(|(l, e)| e.weight * l.re / n_mc)
523                        .collect(),
524                    result_gradient
525                        .par_iter()
526                        .zip(mc_dataset.events.par_iter())
527                        .map(|(grad_l, e)| grad_l.map(|g| g.re).scale(e.weight / n_mc))
528                        .collect(),
529                )
530            };
531            #[cfg(not(feature = "rayon"))]
532            let (res, res_gradient) = {
533                (
534                    result
535                        .iter()
536                        .zip(mc_dataset.events.iter())
537                        .map(|(l, e)| e.weight * l.re / n_mc)
538                        .collect(),
539                    result_gradient
540                        .iter()
541                        .zip(mc_dataset.events.iter())
542                        .map(|(grad_l, e)| grad_l.map(|g| g.re).scale(e.weight / n_mc))
543                        .collect(),
544                )
545            };
546            mc_evaluator.resources.write().active = current_active_mc;
547            Ok((res, res_gradient))
548        } else {
549            let current_active_data = self.data_evaluator.resources.read().active.clone();
550            let current_active_accmc = self.accmc_evaluator.resources.read().active.clone();
551            self.isolate_many(names)?;
552            let mc_dataset = &self.accmc_evaluator.dataset;
553            let result = self.accmc_evaluator.evaluate_local(parameters);
554            let result_gradient = self.accmc_evaluator.evaluate_gradient(parameters);
555            let n_mc = self.accmc_evaluator.dataset.n_events_weighted();
556            #[cfg(feature = "rayon")]
557            let (res, res_gradient) = {
558                (
559                    result
560                        .par_iter()
561                        .zip(mc_dataset.events.par_iter())
562                        .map(|(l, e)| e.weight * l.re / n_mc)
563                        .collect(),
564                    result_gradient
565                        .par_iter()
566                        .zip(mc_dataset.events.par_iter())
567                        .map(|(grad_l, e)| grad_l.map(|g| g.re).scale(e.weight / n_mc))
568                        .collect(),
569                )
570            };
571            #[cfg(not(feature = "rayon"))]
572            let (res, res_gradient) = {
573                (
574                    result
575                        .iter()
576                        .zip(mc_dataset.events.iter())
577                        .map(|(l, e)| e.weight * l.re / n_mc)
578                        .collect(),
579                    result_gradient
580                        .iter()
581                        .zip(mc_dataset.events.iter())
582                        .map(|(grad_l, e)| grad_l.map(|g| g.re).scale(e.weight / n_mc))
583                        .collect(),
584                )
585            };
586            self.data_evaluator.resources.write().active = current_active_data;
587            self.accmc_evaluator.resources.write().active = current_active_accmc;
588            Ok((res, res_gradient))
589        }
590    }
591
592    /// Project the stored [`Model`] over the events in the [`Dataset`] stored by the
593    /// [`Evaluator`] with the given values for free parameters to obtain weights and gradients of
594    /// those weights for each Monte-Carlo event. This method differs from the standard
595    /// [`NLL::project_gradient`] in that it first isolates the selected [`Amplitude`](`laddu_core::amplitudes::Amplitude`)s
596    /// by name, but returns the [`NLL`] to its prior state after calculation (MPI-compatible version).
597    ///
598    /// # Notes
599    ///
600    /// This method is not intended to be called in analyses but rather in writing methods
601    /// that have `mpi`-feature-gated versions. Most users will want to call [`NLL::project_with`] instead.
602    #[cfg(feature = "mpi")]
603    pub fn project_gradient_with_mpi<T: AsRef<str>>(
604        &self,
605        parameters: &[Float],
606        names: &[T],
607        mc_evaluator: Option<Evaluator>,
608        world: &SimpleCommunicator,
609    ) -> Result<(Vec<Float>, Vec<DVector<Float>>), LadduError> {
610        let n_events = mc_evaluator
611            .as_ref()
612            .unwrap_or(&self.accmc_evaluator)
613            .dataset
614            .n_events();
615        let (local_projection, local_gradient_projection) =
616            self.project_gradient_with_local(parameters, names, mc_evaluator)?;
617        let mut projection_result: Vec<Float> = vec![0.0; n_events];
618        let (counts, displs) = world.get_counts_displs(n_events);
619        {
620            let mut partitioned_buffer = PartitionMut::new(&mut projection_result, counts, displs);
621            world.all_gather_varcount_into(&local_projection, &mut partitioned_buffer);
622        }
623
624        let flattened_local_gradient_projection = local_gradient_projection
625            .iter()
626            .flat_map(|g| g.data.as_vec().to_vec())
627            .collect::<Vec<Float>>();
628        let (counts, displs) = world.get_flattened_counts_displs(n_events, parameters.len());
629        let mut flattened_result_buffer = vec![0.0; n_events * parameters.len()];
630        let mut partitioned_flattened_result_buffer =
631            PartitionMut::new(&mut flattened_result_buffer, counts, displs);
632        world.all_gather_varcount_into(
633            &flattened_local_gradient_projection,
634            &mut partitioned_flattened_result_buffer,
635        );
636        let gradient_projection_result = flattened_result_buffer
637            .chunks(parameters.len())
638            .map(DVector::from_row_slice)
639            .collect();
640        Ok((projection_result, gradient_projection_result))
641    }
642    /// Project the stored [`Model`] over the events in the [`Dataset`] stored by the
643    /// [`Evaluator`] with the given values for free parameters to obtain weights and gradients of
644    /// those weights for each
645    /// Monte-Carlo event. This method differs from the standard [`NLL::project_gradient`] in that it first
646    /// isolates the selected [`Amplitude`](`laddu_core::amplitudes::Amplitude`)s by name, but returns
647    /// the [`NLL`] to its prior state after calculation.
648    ///
649    /// This method takes the real part of the given expression (discarding
650    /// the imaginary part entirely, which does not matter if expressions are coherent sums
651    /// wrapped in [`Expression::norm_sqr`](`laddu_core::Expression::norm_sqr`).
652    /// Event weights are determined by the following formula:
653    ///
654    /// ```math
655    /// \text{weight}(\vec{p}; e) = \text{weight}(e) \mathcal{L}(e) / N_{\text{MC}}
656    /// ```
657    ///
658    /// Note that $`N_{\text{MC}}`$ will always be the number of accepted Monte Carlo events,
659    /// regardless of the `mc_evaluator`.
660    pub fn project_gradient_with<T: AsRef<str>>(
661        &self,
662        parameters: &[Float],
663        names: &[T],
664        mc_evaluator: Option<Evaluator>,
665    ) -> Result<(Vec<Float>, Vec<DVector<Float>>), LadduError> {
666        #[cfg(feature = "mpi")]
667        {
668            if let Some(world) = laddu_core::mpi::get_world() {
669                return self.project_gradient_with_mpi(parameters, names, mc_evaluator, &world);
670            }
671        }
672        self.project_gradient_with_local(parameters, names, mc_evaluator)
673    }
674
675    fn evaluate_local(&self, parameters: &[Float]) -> Float {
676        let data_result = self.data_evaluator.evaluate_local(parameters);
677        let mc_result = self.accmc_evaluator.evaluate_local(parameters);
678        let n_mc = self.accmc_evaluator.dataset.n_events_weighted();
679        #[cfg(feature = "rayon")]
680        let data_term: Float = data_result
681            .par_iter()
682            .zip(self.data_evaluator.dataset.events.par_iter())
683            .map(|(l, e)| e.weight * Float::ln(l.re))
684            .parallel_sum_with_accumulator::<Klein<Float>>();
685        #[cfg(feature = "rayon")]
686        let mc_term: Float = mc_result
687            .par_iter()
688            .zip(self.accmc_evaluator.dataset.events.par_iter())
689            .map(|(l, e)| e.weight * l.re)
690            .parallel_sum_with_accumulator::<Klein<Float>>();
691        #[cfg(not(feature = "rayon"))]
692        let data_term: Float = data_result
693            .iter()
694            .zip(self.data_evaluator.dataset.events.iter())
695            .map(|(l, e)| e.weight * Float::ln(l.re))
696            .sum_with_accumulator::<Klein<Float>>();
697        #[cfg(not(feature = "rayon"))]
698        let mc_term: Float = mc_result
699            .iter()
700            .zip(self.accmc_evaluator.dataset.events.iter())
701            .map(|(l, e)| e.weight * l.re)
702            .sum_with_accumulator::<Klein<Float>>();
703        -2.0 * (data_term - mc_term / n_mc)
704    }
705
706    #[cfg(feature = "mpi")]
707    fn evaluate_mpi(&self, parameters: &[Float], world: &SimpleCommunicator) -> Float {
708        let local_evaluation = self.evaluate_local(parameters);
709        let mut buffer: Vec<Float> = vec![0.0; world.size() as usize];
710        world.all_gather_into(&local_evaluation, &mut buffer);
711        buffer.iter().sum()
712    }
713
714    fn evaluate_gradient_local(&self, parameters: &[Float]) -> DVector<Float> {
715        let data_resources = self.data_evaluator.resources.read();
716        let data_parameters = Parameters::new(parameters, &data_resources.constants);
717        let mc_resources = self.accmc_evaluator.resources.read();
718        let mc_parameters = Parameters::new(parameters, &mc_resources.constants);
719        let n_mc = self.accmc_evaluator.dataset.n_events_weighted();
720        #[cfg(feature = "rayon")]
721        let data_term: DVector<Float> = self
722            .data_evaluator
723            .dataset
724            .events
725            .par_iter()
726            .zip(data_resources.caches.par_iter())
727            .map(|(event, cache)| {
728                let mut gradient_values =
729                    vec![DVector::zeros(parameters.len()); self.data_evaluator.amplitudes.len()];
730                self.data_evaluator
731                    .amplitudes
732                    .iter()
733                    .zip(data_resources.active.iter())
734                    .zip(gradient_values.iter_mut())
735                    .for_each(|((amp, active), grad)| {
736                        if *active {
737                            amp.compute_gradient(&data_parameters, event, cache, grad)
738                        }
739                    });
740                (
741                    event.weight,
742                    AmplitudeValues(
743                        self.data_evaluator
744                            .amplitudes
745                            .iter()
746                            .zip(data_resources.active.iter())
747                            .map(|(amp, active)| {
748                                if *active {
749                                    amp.compute(&data_parameters, event, cache)
750                                } else {
751                                    Complex::ZERO
752                                }
753                            })
754                            .collect(),
755                    ),
756                    GradientValues(parameters.len(), gradient_values),
757                )
758            })
759            .map(|(weight, amp_vals, grad_vals)| {
760                (
761                    weight,
762                    self.data_evaluator.expression.evaluate(&amp_vals),
763                    self.data_evaluator
764                        .expression
765                        .evaluate_gradient(&amp_vals, &grad_vals),
766                )
767            })
768            .map(|(w, l, g)| g.map(|gi| gi.re * w / l.re))
769            .collect::<Vec<DVector<Float>>>()
770            .iter()
771            .sum(); // TODO: replace with custom implementation of accurate crate's trait
772        #[cfg(feature = "rayon")]
773        let mc_term: DVector<Float> = self
774            .accmc_evaluator
775            .dataset
776            .events
777            .par_iter()
778            .zip(mc_resources.caches.par_iter())
779            .map(|(event, cache)| {
780                let mut gradient_values =
781                    vec![DVector::zeros(parameters.len()); self.accmc_evaluator.amplitudes.len()];
782                self.accmc_evaluator
783                    .amplitudes
784                    .iter()
785                    .zip(mc_resources.active.iter())
786                    .zip(gradient_values.iter_mut())
787                    .for_each(|((amp, active), grad)| {
788                        if *active {
789                            amp.compute_gradient(&mc_parameters, event, cache, grad)
790                        }
791                    });
792                (
793                    event.weight,
794                    AmplitudeValues(
795                        self.accmc_evaluator
796                            .amplitudes
797                            .iter()
798                            .zip(mc_resources.active.iter())
799                            .map(|(amp, active)| {
800                                if *active {
801                                    amp.compute(&mc_parameters, event, cache)
802                                } else {
803                                    Complex::ZERO
804                                }
805                            })
806                            .collect(),
807                    ),
808                    GradientValues(parameters.len(), gradient_values),
809                )
810            })
811            .map(|(weight, amp_vals, grad_vals)| {
812                (
813                    weight,
814                    self.accmc_evaluator
815                        .expression
816                        .evaluate_gradient(&amp_vals, &grad_vals),
817                )
818            })
819            .map(|(w, g)| w * g.map(|gi| gi.re))
820            .collect::<Vec<DVector<Float>>>()
821            .iter()
822            .sum();
823        #[cfg(not(feature = "rayon"))]
824        let data_term: DVector<Float> = self
825            .data_evaluator
826            .dataset
827            .events
828            .iter()
829            .zip(data_resources.caches.iter())
830            .map(|(event, cache)| {
831                let mut gradient_values =
832                    vec![DVector::zeros(parameters.len()); self.data_evaluator.amplitudes.len()];
833                self.data_evaluator
834                    .amplitudes
835                    .iter()
836                    .zip(data_resources.active.iter())
837                    .zip(gradient_values.iter_mut())
838                    .for_each(|((amp, active), grad)| {
839                        if *active {
840                            amp.compute_gradient(&data_parameters, event, cache, grad)
841                        }
842                    });
843                (
844                    event.weight,
845                    AmplitudeValues(
846                        self.data_evaluator
847                            .amplitudes
848                            .iter()
849                            .zip(data_resources.active.iter())
850                            .map(|(amp, active)| {
851                                if *active {
852                                    amp.compute(&data_parameters, event, cache)
853                                } else {
854                                    Complex::ZERO
855                                }
856                            })
857                            .collect(),
858                    ),
859                    GradientValues(parameters.len(), gradient_values),
860                )
861            })
862            .map(|(weight, amp_vals, grad_vals)| {
863                (
864                    weight,
865                    self.data_evaluator.expression.evaluate(&amp_vals),
866                    self.data_evaluator
867                        .expression
868                        .evaluate_gradient(&amp_vals, &grad_vals),
869                )
870            })
871            .map(|(w, l, g)| g.map(|gi| gi.re * w / l.re))
872            .sum();
873        #[cfg(not(feature = "rayon"))]
874        let mc_term: DVector<Float> = self
875            .accmc_evaluator
876            .dataset
877            .events
878            .iter()
879            .zip(mc_resources.caches.iter())
880            .map(|(event, cache)| {
881                let mut gradient_values =
882                    vec![DVector::zeros(parameters.len()); self.accmc_evaluator.amplitudes.len()];
883                self.accmc_evaluator
884                    .amplitudes
885                    .iter()
886                    .zip(mc_resources.active.iter())
887                    .zip(gradient_values.iter_mut())
888                    .for_each(|((amp, active), grad)| {
889                        if *active {
890                            amp.compute_gradient(&mc_parameters, event, cache, grad)
891                        }
892                    });
893                (
894                    event.weight,
895                    AmplitudeValues(
896                        self.accmc_evaluator
897                            .amplitudes
898                            .iter()
899                            .zip(mc_resources.active.iter())
900                            .map(|(amp, active)| {
901                                if *active {
902                                    amp.compute(&mc_parameters, event, cache)
903                                } else {
904                                    Complex::ZERO
905                                }
906                            })
907                            .collect(),
908                    ),
909                    GradientValues(parameters.len(), gradient_values),
910                )
911            })
912            .map(|(weight, amp_vals, grad_vals)| {
913                (
914                    weight,
915                    self.accmc_evaluator
916                        .expression
917                        .evaluate_gradient(&amp_vals, &grad_vals),
918                )
919            })
920            .map(|(w, g)| w * g.map(|gi| gi.re))
921            .sum();
922        -2.0 * (data_term - mc_term / n_mc)
923    }
924
925    #[cfg(feature = "mpi")]
926    fn evaluate_gradient_mpi(
927        &self,
928        parameters: &[Float],
929        world: &SimpleCommunicator,
930    ) -> DVector<Float> {
931        let local_evaluation_vec = self
932            .evaluate_gradient_local(parameters)
933            .data
934            .as_vec()
935            .to_vec();
936        let mut flattened_result_buffer = vec![0.0; world.size() as usize * parameters.len()];
937        world.all_gather_into(&local_evaluation_vec, &mut flattened_result_buffer);
938        flattened_result_buffer
939            .chunks(parameters.len())
940            .map(DVector::from_row_slice)
941            .sum::<DVector<Float>>()
942    }
943}
944
945impl LikelihoodTerm for NLL {
946    /// Get the list of parameter names in the order they appear in the [`NLL::evaluate`]
947    /// method.
948    fn parameters(&self) -> Vec<String> {
949        self.data_evaluator
950            .resources
951            .read()
952            .parameters
953            .iter()
954            .cloned()
955            .collect()
956    }
957
958    /// Evaluate the stored [`Model`] over the events in the [`Dataset`] stored by the
959    /// [`Evaluator`] with the given values for free parameters. This method takes the
960    /// real part of the given expression (discarding the imaginary part entirely, which
961    /// does not matter if expressions are coherent sums wrapped in [`Expression::norm_sqr`](`laddu_core::Expression::norm_sqr`). The
962    /// result is given by the following formula:
963    ///
964    /// ```math
965    /// NLL(\vec{p}) = -2 \left(\sum_{e \in \text{Data}} \text{weight}(e) \ln(\mathcal{L}(e)) - \frac{1}{N_{\text{MC}_A}} \sum_{e \in \text{MC}_A} \text{weight}(e) \mathcal{L}(e) \right)
966    /// ```
967    fn evaluate(&self, parameters: &[Float]) -> Float {
968        #[cfg(feature = "mpi")]
969        {
970            if let Some(world) = laddu_core::mpi::get_world() {
971                return self.evaluate_mpi(parameters, &world);
972            }
973        }
974        self.evaluate_local(parameters)
975    }
976
977    /// Evaluate the gradient of the stored [`Model`] over the events in the [`Dataset`]
978    /// stored by the [`Evaluator`] with the given values for free parameters. This method takes the
979    /// real part of the given expression (discarding the imaginary part entirely, which
980    /// does not matter if expressions are coherent sums wrapped in
981    /// [`Expression::norm_sqr`](`laddu_core::Expression::norm_sqr`).
982    fn evaluate_gradient(&self, parameters: &[Float]) -> DVector<Float> {
983        #[cfg(feature = "mpi")]
984        {
985            if let Some(world) = laddu_core::mpi::get_world() {
986                return self.evaluate_gradient_mpi(parameters, &world);
987            }
988        }
989        self.evaluate_gradient_local(parameters)
990    }
991}
992
993/// A stochastic [`NLL`] term.
994///
995/// While a regular [`NLL`] will operate over the entire dataset, this term will only operate over
996/// a random subset of the data, determined by the `batch_size` parameter. This will make the
997/// objective function faster to evaluate at the cost of adding random noise to the likelihood.
998#[derive(Clone)]
999pub struct StochasticNLL {
1000    /// A handle to the original [`NLL`] term.
1001    pub nll: NLL,
1002    n: usize,
1003    batch_size: usize,
1004    batch_indices: Arc<Mutex<Vec<usize>>>,
1005    rng: Arc<Mutex<Rng>>,
1006}
1007
1008impl LikelihoodTerm for StochasticNLL {
1009    fn parameters(&self) -> Vec<String> {
1010        self.nll.parameters()
1011    }
1012    fn evaluate(&self, parameters: &[Float]) -> Float {
1013        #[cfg(feature = "mpi")]
1014        {
1015            if let Some(world) = laddu_core::mpi::get_world() {
1016                return self.evaluate_mpi(parameters, &self.batch_indices.lock(), &world);
1017            }
1018        }
1019        #[cfg(feature = "rayon")]
1020        let n_data_batch_local = self
1021            .batch_indices
1022            .lock()
1023            .par_iter()
1024            .map(|&i| self.nll.data_evaluator.dataset.events[i].weight)
1025            .parallel_sum_with_accumulator::<Klein<Float>>();
1026        #[cfg(not(feature = "rayon"))]
1027        let n_data_batch_local = self
1028            .batch_indices
1029            .lock()
1030            .iter()
1031            .map(|&i| self.nll.data_evaluator.dataset.events[i].weight)
1032            .sum_with_accumulator::<Klein<Float>>();
1033        self.evaluate_local(parameters, &self.batch_indices.lock(), n_data_batch_local)
1034    }
1035
1036    fn evaluate_gradient(&self, parameters: &[Float]) -> DVector<Float> {
1037        #[cfg(feature = "mpi")]
1038        {
1039            if let Some(world) = laddu_core::mpi::get_world() {
1040                return self.evaluate_gradient_mpi(parameters, &self.batch_indices.lock(), &world);
1041            }
1042        }
1043        #[cfg(feature = "rayon")]
1044        let n_data_batch_local = self
1045            .batch_indices
1046            .lock()
1047            .par_iter()
1048            .map(|&i| self.nll.data_evaluator.dataset.events[i].weight)
1049            .parallel_sum_with_accumulator::<Klein<Float>>();
1050        #[cfg(not(feature = "rayon"))]
1051        let n_data_batch_local = self
1052            .batch_indices
1053            .lock()
1054            .iter()
1055            .map(|&i| self.nll.data_evaluator.dataset.events[i].weight)
1056            .sum_with_accumulator::<Klein<Float>>();
1057        self.evaluate_gradient_local(parameters, &self.batch_indices.lock(), n_data_batch_local)
1058    }
1059    fn update(&self) {
1060        self.resample();
1061    }
1062}
1063
1064impl StochasticNLL {
1065    /// Generate a new [`StochasticNLL`] with the given [`NLL`], batch size, and optional random seed
1066    ///
1067    /// # See Also
1068    ///
1069    /// [`NLL::to_stochastic`]
1070    pub fn new(nll: NLL, batch_size: usize, seed: Option<usize>) -> Self {
1071        let mut rng = seed.map_or_else(Rng::new, |seed| Rng::with_seed(seed as u64));
1072        let n = nll.data_evaluator.dataset.n_events();
1073        assert!(batch_size > 0 && batch_size <= n);
1074        let batch_indices = rng.subset(batch_size, n);
1075        Self {
1076            nll,
1077            n,
1078            batch_size,
1079            batch_indices: Arc::new(Mutex::new(batch_indices)),
1080            rng: Arc::new(Mutex::new(rng)),
1081        }
1082    }
1083    /// Resample the batch indices used in evaluation
1084    pub fn resample(&self) {
1085        let mut rng = self.rng.lock();
1086        *self.batch_indices.lock() = rng.subset(self.batch_size, self.n);
1087    }
1088    fn evaluate_local(
1089        &self,
1090        parameters: &[Float],
1091        indices: &[usize],
1092        n_data_batch: Float,
1093    ) -> Float {
1094        let data_result = self
1095            .nll
1096            .data_evaluator
1097            .evaluate_batch_local(parameters, indices);
1098        let mc_result = self.nll.accmc_evaluator.evaluate_local(parameters);
1099        let n_mc = self.nll.accmc_evaluator.dataset.n_events_weighted();
1100        let n_data_total = self.nll.data_evaluator.dataset.n_events_weighted();
1101        #[cfg(feature = "rayon")]
1102        {
1103            let data_term: Float = indices
1104                .par_iter()
1105                .map(|&i| {
1106                    let e = &self.nll.data_evaluator.dataset.events[i];
1107                    let l = data_result[i];
1108                    e.weight * l.re.ln()
1109                })
1110                .parallel_sum_with_accumulator::<Klein<Float>>();
1111            let mc_term: Float = mc_result
1112                .par_iter()
1113                .zip(self.nll.accmc_evaluator.dataset.events.par_iter())
1114                .map(|(l, e)| e.weight * l.re)
1115                .parallel_sum_with_accumulator::<Klein<Float>>();
1116            -2.0 * (data_term * n_data_total / n_data_batch - mc_term / n_mc)
1117        }
1118        #[cfg(not(feature = "rayon"))]
1119        {
1120            let data_term: Float = indices
1121                .iter()
1122                .map(|&i| {
1123                    let e = &self.nll.data_evaluator.dataset.events[i];
1124                    let l = data_result[i];
1125                    e.weight * l.re.ln()
1126                })
1127                .sum_with_accumulator::<Klein<Float>>();
1128            let mc_term: Float = mc_result
1129                .iter()
1130                .zip(self.nll.accmc_evaluator.dataset.events.iter())
1131                .map(|(l, e)| e.weight * l.re)
1132                .sum_with_accumulator::<Klein<Float>>();
1133            -2.0 * (data_term * n_data_total / n_data_batch - mc_term / n_mc)
1134        }
1135    }
1136
1137    #[cfg(feature = "mpi")]
1138    fn evaluate_mpi(
1139        &self,
1140        parameters: &[Float],
1141        indices: &[usize],
1142        world: &SimpleCommunicator,
1143    ) -> Float {
1144        let (_, _, locals) = self
1145            .nll
1146            .data_evaluator
1147            .dataset
1148            .get_counts_displs_locals_from_indices(indices, world);
1149        let mut n_data_batch_partitioned: Vec<Float> = vec![0.0; world.size() as usize];
1150        #[cfg(feature = "rayon")]
1151        let n_data_batch_local = indices
1152            .par_iter()
1153            .map(|&i| self.nll.data_evaluator.dataset.events[i].weight)
1154            .parallel_sum_with_accumulator::<Klein<Float>>();
1155        #[cfg(not(feature = "rayon"))]
1156        let n_data_batch_local = indices
1157            .iter()
1158            .map(|&i| self.nll.data_evaluator.dataset.events[i].weight)
1159            .sum_with_accumulator::<Klein<Float>>();
1160        world.all_gather_into(&n_data_batch_local, &mut n_data_batch_partitioned);
1161        #[cfg(feature = "rayon")]
1162        let n_data_batch = n_data_batch_partitioned
1163            .into_par_iter()
1164            .parallel_sum_with_accumulator::<Klein<Float>>();
1165        #[cfg(not(feature = "rayon"))]
1166        let n_data_batch = n_data_batch_partitioned
1167            .into_iter()
1168            .sum_with_accumulator::<Klein<Float>>();
1169        let local_evaluation = self.evaluate_local(parameters, &locals, n_data_batch);
1170        let mut buffer: Vec<Float> = vec![0.0; world.size() as usize];
1171        world.all_gather_into(&local_evaluation, &mut buffer);
1172        buffer.iter().sum()
1173    }
1174
1175    fn evaluate_gradient_local(
1176        &self,
1177        parameters: &[Float],
1178        indices: &[usize],
1179        n_data_batch: Float,
1180    ) -> DVector<Float> {
1181        let data_resources = self.nll.data_evaluator.resources.read();
1182        let data_parameters = Parameters::new(parameters, &data_resources.constants);
1183        let mc_resources = self.nll.accmc_evaluator.resources.read();
1184        let mc_parameters = Parameters::new(parameters, &mc_resources.constants);
1185        let n_data_total = self.nll.data_evaluator.dataset.n_events_weighted();
1186        let n_mc = self.nll.accmc_evaluator.dataset.n_events_weighted();
1187        #[cfg(feature = "rayon")]
1188        let data_term: DVector<Float> = self
1189            .nll
1190            .data_evaluator
1191            .dataset
1192            .events
1193            .par_iter()
1194            .zip(data_resources.caches.par_iter())
1195            .enumerate()
1196            .filter_map(|(i, (event, cache))| {
1197                if indices.contains(&i) {
1198                    Some((event, cache))
1199                } else {
1200                    None
1201                }
1202            })
1203            .map(|(event, cache)| {
1204                let mut gradient_values = vec![
1205                    DVector::zeros(parameters.len());
1206                    self.nll.data_evaluator.amplitudes.len()
1207                ];
1208                self.nll
1209                    .data_evaluator
1210                    .amplitudes
1211                    .iter()
1212                    .zip(data_resources.active.iter())
1213                    .zip(gradient_values.iter_mut())
1214                    .for_each(|((amp, active), grad)| {
1215                        if *active {
1216                            amp.compute_gradient(&data_parameters, event, cache, grad)
1217                        }
1218                    });
1219                (
1220                    event.weight,
1221                    AmplitudeValues(
1222                        self.nll
1223                            .data_evaluator
1224                            .amplitudes
1225                            .iter()
1226                            .zip(data_resources.active.iter())
1227                            .map(|(amp, active)| {
1228                                if *active {
1229                                    amp.compute(&data_parameters, event, cache)
1230                                } else {
1231                                    Complex::ZERO
1232                                }
1233                            })
1234                            .collect(),
1235                    ),
1236                    GradientValues(parameters.len(), gradient_values),
1237                )
1238            })
1239            .map(|(weight, amp_vals, grad_vals)| {
1240                (
1241                    weight,
1242                    self.nll.data_evaluator.expression.evaluate(&amp_vals),
1243                    self.nll
1244                        .data_evaluator
1245                        .expression
1246                        .evaluate_gradient(&amp_vals, &grad_vals),
1247                )
1248            })
1249            .map(|(w, l, g)| g.map(|gi| gi.re * w / l.re))
1250            .collect::<Vec<DVector<Float>>>()
1251            .iter()
1252            .sum(); // TODO: replace with custom implementation of accurate crate's trait
1253        #[cfg(feature = "rayon")]
1254        let mc_term: DVector<Float> = self
1255            .nll
1256            .accmc_evaluator
1257            .dataset
1258            .events
1259            .par_iter()
1260            .zip(mc_resources.caches.par_iter())
1261            .map(|(event, cache)| {
1262                let mut gradient_values = vec![
1263                    DVector::zeros(parameters.len());
1264                    self.nll.accmc_evaluator.amplitudes.len()
1265                ];
1266                self.nll
1267                    .accmc_evaluator
1268                    .amplitudes
1269                    .iter()
1270                    .zip(mc_resources.active.iter())
1271                    .zip(gradient_values.iter_mut())
1272                    .for_each(|((amp, active), grad)| {
1273                        if *active {
1274                            amp.compute_gradient(&mc_parameters, event, cache, grad)
1275                        }
1276                    });
1277                (
1278                    event.weight,
1279                    AmplitudeValues(
1280                        self.nll
1281                            .accmc_evaluator
1282                            .amplitudes
1283                            .iter()
1284                            .zip(mc_resources.active.iter())
1285                            .map(|(amp, active)| {
1286                                if *active {
1287                                    amp.compute(&mc_parameters, event, cache)
1288                                } else {
1289                                    Complex::ZERO
1290                                }
1291                            })
1292                            .collect(),
1293                    ),
1294                    GradientValues(parameters.len(), gradient_values),
1295                )
1296            })
1297            .map(|(weight, amp_vals, grad_vals)| {
1298                (
1299                    weight,
1300                    self.nll
1301                        .accmc_evaluator
1302                        .expression
1303                        .evaluate_gradient(&amp_vals, &grad_vals),
1304                )
1305            })
1306            .map(|(w, g)| w * g.map(|gi| gi.re))
1307            .collect::<Vec<DVector<Float>>>()
1308            .iter()
1309            .sum();
1310        #[cfg(not(feature = "rayon"))]
1311        let data_term: DVector<Float> = self
1312            .nll
1313            .data_evaluator
1314            .dataset
1315            .events
1316            .iter()
1317            .zip(data_resources.caches.iter())
1318            .enumerate()
1319            .filter_map(|(i, (event, cache))| {
1320                if indices.contains(&i) {
1321                    Some((event, cache))
1322                } else {
1323                    None
1324                }
1325            })
1326            .map(|(event, cache)| {
1327                let mut gradient_values = vec![
1328                    DVector::zeros(parameters.len());
1329                    self.nll.data_evaluator.amplitudes.len()
1330                ];
1331                self.nll
1332                    .data_evaluator
1333                    .amplitudes
1334                    .iter()
1335                    .zip(data_resources.active.iter())
1336                    .zip(gradient_values.iter_mut())
1337                    .for_each(|((amp, active), grad)| {
1338                        if *active {
1339                            amp.compute_gradient(&data_parameters, event, cache, grad)
1340                        }
1341                    });
1342                (
1343                    event.weight,
1344                    AmplitudeValues(
1345                        self.nll
1346                            .data_evaluator
1347                            .amplitudes
1348                            .iter()
1349                            .zip(data_resources.active.iter())
1350                            .map(|(amp, active)| {
1351                                if *active {
1352                                    amp.compute(&data_parameters, event, cache)
1353                                } else {
1354                                    Complex::ZERO
1355                                }
1356                            })
1357                            .collect(),
1358                    ),
1359                    GradientValues(parameters.len(), gradient_values),
1360                )
1361            })
1362            .map(|(weight, amp_vals, grad_vals)| {
1363                (
1364                    weight,
1365                    self.nll.data_evaluator.expression.evaluate(&amp_vals),
1366                    self.nll
1367                        .data_evaluator
1368                        .expression
1369                        .evaluate_gradient(&amp_vals, &grad_vals),
1370                )
1371            })
1372            .map(|(w, l, g)| g.map(|gi| gi.re * w / l.re))
1373            .sum();
1374        #[cfg(not(feature = "rayon"))]
1375        let mc_term: DVector<Float> = self
1376            .nll
1377            .accmc_evaluator
1378            .dataset
1379            .events
1380            .iter()
1381            .zip(mc_resources.caches.iter())
1382            .map(|(event, cache)| {
1383                let mut gradient_values = vec![
1384                    DVector::zeros(parameters.len());
1385                    self.nll.accmc_evaluator.amplitudes.len()
1386                ];
1387                self.nll
1388                    .accmc_evaluator
1389                    .amplitudes
1390                    .iter()
1391                    .zip(mc_resources.active.iter())
1392                    .zip(gradient_values.iter_mut())
1393                    .for_each(|((amp, active), grad)| {
1394                        if *active {
1395                            amp.compute_gradient(&mc_parameters, event, cache, grad)
1396                        }
1397                    });
1398                (
1399                    event.weight,
1400                    AmplitudeValues(
1401                        self.nll
1402                            .accmc_evaluator
1403                            .amplitudes
1404                            .iter()
1405                            .zip(mc_resources.active.iter())
1406                            .map(|(amp, active)| {
1407                                if *active {
1408                                    amp.compute(&mc_parameters, event, cache)
1409                                } else {
1410                                    Complex::ZERO
1411                                }
1412                            })
1413                            .collect(),
1414                    ),
1415                    GradientValues(parameters.len(), gradient_values),
1416                )
1417            })
1418            .map(|(weight, amp_vals, grad_vals)| {
1419                (
1420                    weight,
1421                    self.nll
1422                        .accmc_evaluator
1423                        .expression
1424                        .evaluate_gradient(&amp_vals, &grad_vals),
1425                )
1426            })
1427            .map(|(w, g)| w * g.map(|gi| gi.re))
1428            .sum();
1429        -2.0 * (data_term * n_data_total / n_data_batch - mc_term / n_mc)
1430    }
1431
1432    #[cfg(feature = "mpi")]
1433    fn evaluate_gradient_mpi(
1434        &self,
1435        parameters: &[Float],
1436        indices: &[usize],
1437        world: &SimpleCommunicator,
1438    ) -> DVector<Float> {
1439        let (_, _, locals) = self
1440            .nll
1441            .data_evaluator
1442            .dataset
1443            .get_counts_displs_locals_from_indices(indices, world);
1444        let mut n_data_batch_partitioned: Vec<Float> = vec![0.0; world.size() as usize];
1445        #[cfg(feature = "rayon")]
1446        let n_data_batch_local = indices
1447            .par_iter()
1448            .map(|&i| self.nll.data_evaluator.dataset.events[i].weight)
1449            .parallel_sum_with_accumulator::<Klein<Float>>();
1450        #[cfg(not(feature = "rayon"))]
1451        let n_data_batch_local = indices
1452            .iter()
1453            .map(|&i| self.nll.data_evaluator.dataset.events[i].weight)
1454            .sum_with_accumulator::<Klein<Float>>();
1455        world.all_gather_into(&n_data_batch_local, &mut n_data_batch_partitioned);
1456        #[cfg(feature = "rayon")]
1457        let n_data_batch = n_data_batch_partitioned
1458            .into_par_iter()
1459            .parallel_sum_with_accumulator::<Klein<Float>>();
1460        #[cfg(not(feature = "rayon"))]
1461        let n_data_batch = n_data_batch_partitioned
1462            .into_iter()
1463            .sum_with_accumulator::<Klein<Float>>();
1464        let local_evaluation_vec = self
1465            .evaluate_gradient_local(parameters, &locals, n_data_batch)
1466            .data
1467            .as_vec()
1468            .to_vec();
1469        let mut flattened_result_buffer = vec![0.0; world.size() as usize * parameters.len()];
1470        world.all_gather_into(&local_evaluation_vec, &mut flattened_result_buffer);
1471        flattened_result_buffer
1472            .chunks(parameters.len())
1473            .map(DVector::from_row_slice)
1474            .sum::<DVector<Float>>()
1475    }
1476}
1477
1478/// A (extended) negative log-likelihood evaluator
1479///
1480/// Parameters
1481/// ----------
1482/// model: Model
1483///     The Model to evaluate
1484/// ds_data : Dataset
1485///     A Dataset representing true signal data
1486/// ds_accmc : Dataset
1487///     A Dataset of physically flat accepted Monte Carlo data used for normalization
1488///
1489#[cfg(feature = "python")]
1490#[pyclass(name = "NLL", module = "laddu")]
1491#[derive(Clone)]
1492pub struct PyNLL(pub Box<NLL>);
1493
1494#[cfg(feature = "python")]
1495#[pymethods]
1496impl PyNLL {
1497    #[new]
1498    #[pyo3(signature = (model, ds_data, ds_accmc))]
1499    fn new(model: &PyModel, ds_data: &PyDataset, ds_accmc: &PyDataset) -> Self {
1500        Self(NLL::new(&model.0, &ds_data.0, &ds_accmc.0))
1501    }
1502    /// The underlying signal dataset used in calculating the NLL
1503    ///
1504    /// Returns
1505    /// -------
1506    /// Dataset
1507    ///
1508    #[getter]
1509    fn data(&self) -> PyDataset {
1510        PyDataset(self.0.data_evaluator.dataset.clone())
1511    }
1512    /// The underlying accepted Monte Carlo dataset used in calculating the NLL
1513    ///
1514    /// Returns
1515    /// -------
1516    /// Dataset
1517    ///
1518    #[getter]
1519    fn accmc(&self) -> PyDataset {
1520        PyDataset(self.0.accmc_evaluator.dataset.clone())
1521    }
1522    /// Turn an ``NLL`` into a ``StochasticNLL``
1523    ///
1524    /// Parameters
1525    /// ----------
1526    /// batch_size : int
1527    ///     The batch size for the data
1528    /// seed : int, default=None
1529    ///
1530    /// Returns
1531    /// -------
1532    /// StochasticNLL
1533    ///
1534    #[pyo3(signature = (batch_size, *, seed=None))]
1535    fn to_stochastic(&self, batch_size: usize, seed: Option<usize>) -> PyStochasticNLL {
1536        PyStochasticNLL(self.0.to_stochastic(batch_size, seed))
1537    }
1538    /// Turn an ``NLL`` into a term that can be used by a ``LikelihoodManager``
1539    ///
1540    /// Returns
1541    /// -------
1542    /// term : LikelihoodTerm
1543    ///     The isolated NLL which can be used to build more complex models
1544    ///
1545    ///
1546    fn to_term(&self) -> PyLikelihoodTerm {
1547        PyLikelihoodTerm(self.0.clone())
1548    }
1549    /// The names of the free parameters used to evaluate the NLL
1550    ///
1551    /// Returns
1552    /// -------
1553    /// parameters : list of str
1554    ///
1555    #[getter]
1556    fn parameters(&self) -> Vec<String> {
1557        self.0.parameters()
1558    }
1559    /// Activates Amplitudes in the NLL by name
1560    ///
1561    /// Parameters
1562    /// ----------
1563    /// arg : str or list of str
1564    ///     Names of Amplitudes to be activated
1565    ///
1566    /// Raises
1567    /// ------
1568    /// TypeError
1569    ///     If `arg` is not a str or list of str
1570    /// ValueError
1571    ///     If `arg` or any items of `arg` are not registered Amplitudes
1572    ///
1573    fn activate(&self, arg: &Bound<'_, PyAny>) -> PyResult<()> {
1574        if let Ok(string_arg) = arg.extract::<String>() {
1575            self.0.activate(&string_arg)?;
1576        } else if let Ok(list_arg) = arg.downcast::<PyList>() {
1577            let vec: Vec<String> = list_arg.extract()?;
1578            self.0.activate_many(&vec)?;
1579        } else {
1580            return Err(PyTypeError::new_err(
1581                "Argument must be either a string or a list of strings",
1582            ));
1583        }
1584        Ok(())
1585    }
1586    /// Activates all Amplitudes in the NLL
1587    ///
1588    fn activate_all(&self) {
1589        self.0.activate_all();
1590    }
1591    /// Deactivates Amplitudes in the NLL by name
1592    ///
1593    /// Deactivated Amplitudes act as zeros in the NLL
1594    ///
1595    /// Parameters
1596    /// ----------
1597    /// arg : str or list of str
1598    ///     Names of Amplitudes to be deactivated
1599    ///
1600    /// Raises
1601    /// ------
1602    /// TypeError
1603    ///     If `arg` is not a str or list of str
1604    /// ValueError
1605    ///     If `arg` or any items of `arg` are not registered Amplitudes
1606    ///
1607    fn deactivate(&self, arg: &Bound<'_, PyAny>) -> PyResult<()> {
1608        if let Ok(string_arg) = arg.extract::<String>() {
1609            self.0.deactivate(&string_arg)?;
1610        } else if let Ok(list_arg) = arg.downcast::<PyList>() {
1611            let vec: Vec<String> = list_arg.extract()?;
1612            self.0.deactivate_many(&vec)?;
1613        } else {
1614            return Err(PyTypeError::new_err(
1615                "Argument must be either a string or a list of strings",
1616            ));
1617        }
1618        Ok(())
1619    }
1620    /// Deactivates all Amplitudes in the NLL
1621    ///
1622    fn deactivate_all(&self) {
1623        self.0.deactivate_all();
1624    }
1625    /// Isolates Amplitudes in the NLL by name
1626    ///
1627    /// Activates the Amplitudes given in `arg` and deactivates the rest
1628    ///
1629    /// Parameters
1630    /// ----------
1631    /// arg : str or list of str
1632    ///     Names of Amplitudes to be isolated
1633    ///
1634    /// Raises
1635    /// ------
1636    /// TypeError
1637    ///     If `arg` is not a str or list of str
1638    /// ValueError
1639    ///     If `arg` or any items of `arg` are not registered Amplitudes
1640    ///
1641    fn isolate(&self, arg: &Bound<'_, PyAny>) -> PyResult<()> {
1642        if let Ok(string_arg) = arg.extract::<String>() {
1643            self.0.isolate(&string_arg)?;
1644        } else if let Ok(list_arg) = arg.downcast::<PyList>() {
1645            let vec: Vec<String> = list_arg.extract()?;
1646            self.0.isolate_many(&vec)?;
1647        } else {
1648            return Err(PyTypeError::new_err(
1649                "Argument must be either a string or a list of strings",
1650            ));
1651        }
1652        Ok(())
1653    }
1654    /// Evaluate the extended negative log-likelihood over the stored Datasets
1655    ///
1656    /// This is defined as
1657    ///
1658    /// .. math:: NLL(\vec{p}; D, MC) = -2 \left( \sum_{e \in D} (e_w \log(\mathcal{L}(e))) - \frac{1}{N_{MC}} \sum_{e \in MC} (e_w \mathcal{L}(e)) \right)
1659    ///
1660    /// Parameters
1661    /// ----------
1662    /// parameters : list of float
1663    ///     The values to use for the free parameters
1664    /// threads : int, optional
1665    ///     The number of threads to use (setting this to None will use all available CPUs)
1666    ///
1667    /// Returns
1668    /// -------
1669    /// result : float
1670    ///     The total negative log-likelihood
1671    ///
1672    /// Raises
1673    /// ------
1674    /// Exception
1675    ///     If there was an error building the thread pool
1676    ///
1677    #[pyo3(signature = (parameters, *, threads=None))]
1678    fn evaluate(&self, parameters: Vec<Float>, threads: Option<usize>) -> PyResult<Float> {
1679        #[cfg(feature = "rayon")]
1680        {
1681            Ok(ThreadPoolBuilder::new()
1682                .num_threads(threads.unwrap_or(0))
1683                .build()
1684                .map_err(LadduError::from)?
1685                .install(|| LikelihoodTerm::evaluate(self.0.as_ref(), &parameters)))
1686        }
1687        #[cfg(not(feature = "rayon"))]
1688        {
1689            Ok(LikelihoodTerm::evaluate(self.0.as_ref(), &parameters))
1690        }
1691    }
1692    /// Evaluate the gradient of the negative log-likelihood over the stored Dataset
1693    ///
1694    /// Parameters
1695    /// ----------
1696    /// parameters : list of float
1697    ///     The values to use for the free parameters
1698    /// threads : int, optional
1699    ///     The number of threads to use (setting this to None will use all available CPUs)
1700    ///
1701    /// Returns
1702    /// -------
1703    /// result : array_like
1704    ///     A ``numpy`` array of representing the gradient of the negative log-likelihood over each parameter
1705    ///
1706    /// Raises
1707    /// ------
1708    /// Exception
1709    ///     If there was an error building the thread pool or problem creating the resulting
1710    ///     ``numpy`` array
1711    ///
1712    #[pyo3(signature = (parameters, *, threads=None))]
1713    fn evaluate_gradient<'py>(
1714        &self,
1715        py: Python<'py>,
1716        parameters: Vec<Float>,
1717        threads: Option<usize>,
1718    ) -> PyResult<Bound<'py, PyArray1<Float>>> {
1719        #[cfg(feature = "rayon")]
1720        {
1721            Ok(PyArray1::from_slice(
1722                py,
1723                ThreadPoolBuilder::new()
1724                    .num_threads(threads.unwrap_or(0))
1725                    .build()
1726                    .map_err(LadduError::from)?
1727                    .install(|| self.0.evaluate_gradient(&parameters))
1728                    .as_slice(),
1729            ))
1730        }
1731        #[cfg(not(feature = "rayon"))]
1732        {
1733            Ok(PyArray1::from_slice(
1734                py,
1735                self.0.evaluate_gradient(&parameters).as_slice(),
1736            ))
1737        }
1738    }
1739    /// Project the model over the Monte Carlo dataset with the given parameter values
1740    ///
1741    /// This is defined as
1742    ///
1743    /// .. math:: e_w(\vec{p}) = \frac{e_w}{N_{MC}} \mathcal{L}(e)
1744    ///
1745    /// Parameters
1746    /// ----------
1747    /// parameters : list of float
1748    ///     The values to use for the free parameters
1749    /// mc_evaluator: Evaluator, optional
1750    ///     Project using the given Evaluator or use the stored ``accmc`` if None
1751    /// threads : int, optional
1752    ///     The number of threads to use (setting this to None will use all available CPUs)
1753    ///
1754    /// Returns
1755    /// -------
1756    /// result : array_like
1757    ///     Weights for every Monte Carlo event which represent the fit to data
1758    ///
1759    /// Raises
1760    /// ------
1761    /// Exception
1762    ///     If there was an error building the thread pool or problem creating the resulting
1763    ///     ``numpy`` array
1764    ///
1765    #[pyo3(signature = (parameters, *, mc_evaluator = None, threads=None))]
1766    fn project<'py>(
1767        &self,
1768        py: Python<'py>,
1769        parameters: Vec<Float>,
1770        mc_evaluator: Option<PyEvaluator>,
1771        threads: Option<usize>,
1772    ) -> PyResult<Bound<'py, PyArray1<Float>>> {
1773        #[cfg(feature = "rayon")]
1774        {
1775            Ok(PyArray1::from_slice(
1776                py,
1777                ThreadPoolBuilder::new()
1778                    .num_threads(threads.unwrap_or(0))
1779                    .build()
1780                    .map_err(LadduError::from)?
1781                    .install(|| {
1782                        self.0
1783                            .project(&parameters, mc_evaluator.map(|pyeval| pyeval.0.clone()))
1784                    })
1785                    .as_slice(),
1786            ))
1787        }
1788        #[cfg(not(feature = "rayon"))]
1789        {
1790            Ok(PyArray1::from_slice(
1791                py,
1792                self.0
1793                    .project(&parameters, mc_evaluator.map(|pyeval| pyeval.0.clone()))
1794                    .as_slice(),
1795            ))
1796        }
1797    }
1798
1799    /// Project the model over the Monte Carlo dataset with the given parameter values, first
1800    /// isolating the given terms by name. The NLL is then reset to its previous state of
1801    /// activation.
1802    ///
1803    /// This is defined as
1804    ///
1805    /// .. math:: e_w(\vec{p}) = \frac{e_w}{N_{MC}} \mathcal{L}(e)
1806    ///
1807    /// Parameters
1808    /// ----------
1809    /// parameters : list of float
1810    ///     The values to use for the free parameters
1811    /// arg : str or list of str
1812    ///     Names of Amplitudes to be isolated
1813    /// mc_evaluator: Evaluator, optional
1814    ///     Project using the given Evaluator or use the stored ``accmc`` if None
1815    /// threads : int, optional
1816    ///     The number of threads to use (setting this to None will use all available CPUs)
1817    ///
1818    /// Returns
1819    /// -------
1820    /// result : array_like
1821    ///     Weights for every Monte Carlo event which represent the fit to data
1822    ///
1823    /// Raises
1824    /// ------
1825    /// TypeError
1826    ///     If `arg` is not a str or list of str
1827    ///
1828    /// Raises
1829    /// ------
1830    /// Exception
1831    ///     If there was an error building the thread pool or problem creating the resulting
1832    ///     ``numpy`` array
1833    /// ValueError
1834    ///     If `arg` or any items of `arg` are not registered Amplitudes
1835    ///
1836    #[pyo3(signature = (parameters, arg, *, mc_evaluator = None, threads=None))]
1837    fn project_with<'py>(
1838        &self,
1839        py: Python<'py>,
1840        parameters: Vec<Float>,
1841        arg: &Bound<'_, PyAny>,
1842        mc_evaluator: Option<PyEvaluator>,
1843        threads: Option<usize>,
1844    ) -> PyResult<Bound<'py, PyArray1<Float>>> {
1845        let names = if let Ok(string_arg) = arg.extract::<String>() {
1846            vec![string_arg]
1847        } else if let Ok(list_arg) = arg.downcast::<PyList>() {
1848            let vec: Vec<String> = list_arg.extract()?;
1849            vec
1850        } else {
1851            return Err(PyTypeError::new_err(
1852                "Argument must be either a string or a list of strings",
1853            ));
1854        };
1855        #[cfg(feature = "rayon")]
1856        {
1857            Ok(PyArray1::from_slice(
1858                py,
1859                ThreadPoolBuilder::new()
1860                    .num_threads(threads.unwrap_or(0))
1861                    .build()
1862                    .map_err(LadduError::from)?
1863                    .install(|| {
1864                        self.0.project_with(
1865                            &parameters,
1866                            &names,
1867                            mc_evaluator.map(|pyeval| pyeval.0.clone()),
1868                        )
1869                    })?
1870                    .as_slice(),
1871            ))
1872        }
1873        #[cfg(not(feature = "rayon"))]
1874        {
1875            Ok(PyArray1::from_slice(
1876                py,
1877                self.0
1878                    .project_with(
1879                        &parameters,
1880                        &names,
1881                        mc_evaluator.map(|pyeval| pyeval.0.clone()),
1882                    )?
1883                    .as_slice(),
1884            ))
1885        }
1886    }
1887
1888    /// Minimize the NLL with respect to the free parameters in the model
1889    ///
1890    /// This method "runs the fit". Given an initial position `p0`, this
1891    /// method performs a minimization over the negative log-likelihood, optimizing the model
1892    /// over the stored signal data and Monte Carlo.
1893    ///
1894    /// Parameters
1895    /// ----------
1896    /// p0 : array_like
1897    ///     The initial parameters at the start of optimization
1898    /// bounds : list of tuple of float or None, optional
1899    ///     Optional bounds on each parameter (use None or an infinity for no bound)
1900    /// method : {'lbfgsb', 'nelder-mead', 'adam', 'pso'}
1901    ///     The minimization algorithm to use
1902    /// settings : dict, optional
1903    ///     Settings for the minimization algorithm (see notes)
1904    /// observers : MinimizerObserver or list of MinimizerObserver, optional
1905    ///     User-defined observers which are called at each step
1906    /// terminators : MinimizerTerminator or list of MinimizerTerminator, optional
1907    ///     User-defined terminators which are called at each step
1908    /// max_steps : int, optional
1909    ///     Set the maximum number of steps
1910    /// debug : bool, default=False
1911    ///     Use a debug observer to print out debugging information at each step
1912    /// threads : int, default=0
1913    ///     The number of threads to use (setting this to 0 will use all available CPUs)
1914    ///
1915    /// Returns
1916    /// -------
1917    /// MinimizationSummary
1918    ///     The status of the minimization algorithm at termination
1919    ///
1920    /// Raises
1921    /// ------
1922    /// Exception
1923    ///     If there was an error building the thread pool
1924    ///
1925    /// Notes
1926    /// -----
1927    /// The `settings` dict is passed to the minimization algorithm as keyword arguments. Each
1928    /// algorithm has different settings:
1929    ///
1930    /// L-BFGS-B
1931    /// ========
1932    /// m : int, default=10
1933    ///     The number of saved corrections to the approximated Hessian.
1934    /// skip_hessian : bool, default=False
1935    ///     If True, the exact Hessian will not be calculated.
1936    /// line_search : dict
1937    ///     Settings for the line search (see next section).
1938    /// eps_f : float, default=`MACH_EPS^(1/2)`
1939    ///     The tolerance for stopping based on the change in function value.
1940    /// eps_g : float, default=`MACH_EPS^(1/3)`
1941    ///     The tolerance for stopping based on the change in function gradient.
1942    /// eps_norm_g : float, default=1e-5
1943    ///     The tolerance for stopping based on the change in the infinity-norm of the function gradient.
1944    ///
1945    /// Line Search
1946    /// ===========
1947    /// method : {"morethuente", "hagerzhang"}
1948    ///     The line search method to use.
1949    /// max_iterations : int, default=100
1950    ///     The maximum number of line search iterations.
1951    /// c1 : float, default=1e-4
1952    ///     The first Wolfe condition constant (More-Thuente only).
1953    /// c2 : float, default=0.9
1954    ///     The second Wolfe condition constant (More-Thuente only).
1955    /// max_zoom : int, default=100
1956    ///     The maximum number of zoom steps (More-Thuente only).
1957    /// delta : float, default=0.1
1958    ///     The first Wolfe condition constant (Hager-Zhang only).
1959    /// sigma : float, default=0.9
1960    ///     The second Wolfe condition constant (Hager-Zhang only).
1961    /// epsilon : float, default=`MACH_EPS^(1/3)`
1962    ///     The tolerance parameter on approximate Wolfe termination (Hager-Zhang only).
1963    /// theta : float, default=0.5
1964    ///     The split ratio for interval updates (defaults to bisection) (Hager-Zhang only).
1965    /// gamma : float, default=0.66
1966    ///     A parameter which determines when a bisection is performed (Hager-Zhang only).
1967    /// max_bisects : int, default=50
1968    ///     The maximum number of allowed bisections (Hager-Zhang only).
1969    ///
1970    /// Adam
1971    /// ====
1972    /// beta_c : float, default=0.9
1973    ///     The slope of the exponential moving average used to terminate the algorithm.
1974    /// eps_loss : float, default=`MACH_EPS^(1/2)`
1975    ///     The minimum change in exponential moving average loss which will increase the patience counter.
1976    /// patience : int, default=1
1977    ///     The number of allowed iterations with no improvement in the loss (according to an exponential moving average) before the algorithm terminates.
1978    ///
1979    /// Nelder-Mead
1980    /// ===========
1981    /// alpha : float, default=1.0
1982    ///     The reflection coefficient.
1983    /// beta : float, default=2.0
1984    ///     The expansion coefficient.
1985    /// gamma : float, default=0.5
1986    ///     The contraction coefficient.
1987    /// delta : float, default=0.5
1988    ///     The shrink coefficient.
1989    /// adaptive : bool, default=False
1990    ///     Use adaptive hyperparameters according to Gao and Han[Gao]_.
1991    /// expansion_method : {"greedyminimization", "greedyexpansion"}
1992    ///     Greedy minimization will favor points which minimize faster, but greedy expansion may explore a space more efficiently. See [Lagarias]_ for details.
1993    /// simplex_construction_method : {"scaledorthogonal", "orthogonal", "custom"}
1994    ///     The method used to generate the initial simplex.
1995    /// orthogonal_multiplier : float, default=1.05
1996    ///     Multiplier used on nonzero coordinates of the initial vertex in simplex generation (scaled orthogonal method).
1997    /// orthogonal_zero_step : float, default=0.00025
1998    ///     Value to use for coordinates of the initial vertex which are zero in simplex generation (scaled orthogonal method).
1999    /// simplex_size : float, default=1.0
2000    ///     The step in each orthogonal direction from the initial vertex in simplex generation (orthogonal method).
2001    /// simplex : list of list of floats
2002    ///     Specify the initial simplex directly. Each entry in the list must be a unique point in the parameter space. The initial vertex is also included, so this argument must specify as many vertices as there are dimensions in the parameter space. This must be specified if simplex_construction_method is set to "custom".
2003    /// f_terminator : {"stddev", "absolute", "amoeba"} or None, default="stddev"
2004    ///     Set the method to terminate the algorithm based on the function values over the simplex. See [Singer]_ for details. Set to None to skip this check.
2005    /// eps_f : float, default=`MACH_EPS^(1/4)`
2006    ///     The tolerance for the f_terminator method.
2007    /// x_terminator : {"singer", "diameter", "higham", "rowan"} or None, default="singer"
2008    ///     Set the method to terminate the algorithm based on the position of simplex vertices. See [Singer]_ for details. Set to None to skip this check.
2009    /// eps_x : float, default=`MACH_EPS^(1/4)`
2010    ///     The tolerance for the x_terminator method.
2011    ///
2012    /// Particle Swarm Optimization (PSO)
2013    /// =================================
2014    /// swarm_position_initializer : {"randominlimits", "latinhypercube", "custom"}
2015    ///     The method used to initialize the swarm position. The "randominlimits" and "latinhypercube" methods require swarm_position_bounds and swarm_size to be specified, and they ignore the initial position given when constructing the swarm (this behavior may change in the future). The "custom" method requires swarm to be specified and does include the initial position.
2016    /// swarm_position_bounds : list of tuple of floats or None
2017    ///     Bounds used when randomly generating a swarm with either the "randominlimits" or "latinhypercube" swarm_position_initializer.
2018    /// swarm_size : int
2019    ///     The number of particles in the swarm when using the "randominlimits" or "latinhypercube" swarm_position_initializer.
2020    /// swarm : list of list of floats
2021    ///     A list of positions of each particle in the swarm. This argument is required when using the "custom" swarm_position_initializer.
2022    /// swarm_topology : {"global", "ring"}
2023    ///     The topology connecting particles in the swarm.
2024    /// swarm_update_method : {"sync", "synchronous", "async", "asynchronous"}
2025    ///     Synchronous updates update positions and targets in separate loops (slower but sometimes more stable) while asynchronous updates them in the same loop (faster but sometimes less stable).
2026    /// swarm_boundary_method : {"inf", "shr"}
2027    ///     The boundary method used for the swarm. "inf" sets infeasable values to +inf while "shr" shrinks the velocity vector to place the particle on the boundary where it would cross.
2028    /// use_transform : bool, default=False
2029    ///     If True, the algorithm will ignore the swarm_boundary_method and instead perform a coordinate transformation on the swarm to ensure the swarm is within bounds.
2030    /// swarm_velocity_bounds : list of tuple of floats or None, optional
2031    ///     Bounds used when randomly generating the initial velocity of each particle in the swarm. If not specified, initial velocities are set to zero.
2032    /// omega : float, default=0.8
2033    ///     The inertial weight.
2034    /// c1 : float, default = 0.1
2035    ///     The cognitive weight.
2036    /// c2 : float, default = 0.1
2037    ///     The social weight.
2038    ///
2039    /// .. rubric:: References
2040    ///
2041    /// .. [Gao] F. Gao and L. Han, “Implementing the Nelder-Mead simplex algorithm with adaptive parameters,” Comput Optim Appl, vol. 51, no. 1, pp. 259–277, May 2010, doi: 10.1007/s10589-010-9329-3.
2042    /// .. [Lagarias] J. C. Lagarias, J. A. Reeds, M. H. Wright, and P. E. Wright, “Convergence Properties of the Nelder--Mead Simplex Method in Low Dimensions,” SIAM J. Optim., vol. 9, no. 1, pp. 112–147, Jan. 1998, doi: 10.1137/s1052623496303470.
2043    /// .. [Singer] S. Singer and S. Singer, “Efficient Implementation of the Nelder–Mead Search Algorithm,” Appl Numer Analy &amp; Comput, vol. 1, no. 2, pp. 524–534, Dec. 2004, doi: 10.1002/anac.200410015.
2044    ///
2045    #[pyo3(signature = (p0, *, bounds=None, method="lbfgsb".to_string(), settings=None, observers=None, terminators=None, max_steps=None, debug=false, threads=0))]
2046    #[allow(clippy::too_many_arguments)]
2047    fn minimize<'py>(
2048        &self,
2049        py: Python<'py>,
2050        p0: Vec<Float>,
2051        bounds: Option<Vec<(Option<Float>, Option<Float>)>>,
2052        method: String,
2053        settings: Option<Bound<'py, PyDict>>,
2054        observers: Option<Bound<'_, PyAny>>,
2055        terminators: Option<Bound<'_, PyAny>>,
2056        max_steps: Option<usize>,
2057        debug: bool,
2058        threads: usize,
2059    ) -> PyResult<PyMinimizationSummary> {
2060        let full_settings = PyDict::new(py);
2061        if let Some(bounds) = bounds {
2062            full_settings.set_item("bounds", bounds)?;
2063        }
2064        full_settings.set_item("method", method)?;
2065        if let Some(observers) = observers {
2066            full_settings.set_item("observers", observers)?;
2067        }
2068        if let Some(terminators) = terminators {
2069            full_settings.set_item("terminators", terminators)?;
2070        }
2071        if let Some(max_steps) = max_steps {
2072            full_settings.set_item("max_steps", max_steps)?;
2073        }
2074        full_settings.set_item("debug", debug)?;
2075        full_settings.set_item("threads", threads)?;
2076        if let Some(settings) = settings {
2077            full_settings.set_item("settings", settings)?;
2078        }
2079        let result = self
2080            .0
2081            .minimize(MinimizationSettings::from_pyargs(&p0, &full_settings)?)?;
2082        Ok(PyMinimizationSummary(result))
2083    }
2084
2085    /// Run an MCMC algorithm on the free parameters of the NLL's model
2086    ///
2087    /// This method can be used to sample the underlying log-likelihood given an initial
2088    /// position for each walker `p0`.
2089    ///
2090    /// Parameters
2091    /// ----------
2092    /// p0 : array_like
2093    ///     The initial positions of each walker with dimension (n_walkers, n_parameters)
2094    /// bounds : list of tuple of float or None, optional
2095    ///     Optional bounds on each parameter (use None or an infinity for no bound)
2096    /// method : {'aies', 'ess'}
2097    ///     The MCMC algorithm to use
2098    /// settings : dict, optional
2099    ///     Settings for the MCMC algorithm (see notes)
2100    /// observers : MCMCObserver or list of MCMCObserver, optional
2101    ///     User-defined observers which are called at each step
2102    /// terminators : MCMCTerminator or list of MCMCTerminator, optional
2103    ///     User-defined terminators which are called at each step
2104    /// max_steps : int, optional
2105    ///     Set the maximum number of steps
2106    /// debug : bool, default=False
2107    ///     Use a debug observer to print out debugging information at each step
2108    /// threads : int, default=0
2109    ///     The number of threads to use (setting this to 0 will use all available CPUs)
2110    ///
2111    /// Returns
2112    /// -------
2113    /// MCMCSummary
2114    ///     The status of the MCMC algorithm at termination
2115    ///
2116    /// Raises
2117    /// ------
2118    /// Exception
2119    ///     If there was an error building the thread pool
2120    ///
2121    /// Notes
2122    /// -----
2123    /// The `settings` dict is passed to the MCMC algorithm as keyword arguments. Each
2124    /// algorithm has different settings.
2125    ///
2126    /// AIES (Affine-Invariant Ensemble Sampler) [Goodman]_
2127    /// =============================================
2128    /// moves : list of tuple of (str, float) or (str, dict, float), default = [('stretch', {'a': 2.0}, 1.0)]
2129    ///     The list of moves to use. The first element of the tuple is the name of the move
2130    ///     ('stretch' or 'walk') and the last is the frequency that move should be used relative
2131    ///     to the sum of all frequencies given across all moves. An optional middle dictionary
2132    ///     parameter may be provided to specify properties of moves which support it. For the AIES
2133    ///     algorithm, the stretch move may use the 'a' parameter to specify the scaling parameter
2134    ///     (2.0 by default).
2135    ///
2136    /// ESS (Ensemble Slice Sampler) [Karamanis]_
2137    /// =================================
2138    /// moves : list of tuple of (str, float) or (str, dict, float), default = [('differential', 1.0)]
2139    ///     The list of moves to use. The first element of the tuple is the name of the move
2140    ///     ('differential', 'gaussian', or 'global') and the last is the frequency that move
2141    ///     should be used relative to the sum of all frequencies given across all moves. An
2142    ///     optional middle dictionary parameter may be provided to specify properties of moves
2143    ///     which support it. For the ESS algorithm, the global move may use a 'scale' parameter
2144    ///     (1.0 by default) which rescales steps within a local cluster, a 'rescale_cov' parameter
2145    ///     (0.001 by default) which rescales the covariance matrix of clusters, and an
2146    ///     'n_components' parameter (5 by default) which represents the number of mixture
2147    ///     components to use for clustering (should be larger than the expected number of modes).
2148    /// n_adaptive : int, default=0
2149    ///     The number of adaptive moves to perform at the start of sampling
2150    /// max_steps : int, default=10000
2151    ///     The maximum number of expansions/contractions to perform at each step in the algorithm
2152    /// mu : float, default = 1.0
2153    ///     The adaptive scaling parameter (only applies if 'n_adaptive' is greater than zero)
2154    ///
2155    /// .. rubric:: References
2156    ///
2157    /// .. [Goodman] J. Goodman and J. Weare, “Ensemble samplers with affine invariance,” CAMCoS, vol. 5, no. 1, pp. 65–80, Jan. 2010, doi: 10.2140/camcos.2010.5.65.
2158    /// .. [Karamanis] M. Karamanis and F. Beutler, “Ensemble slice sampling,” Stat Comput, vol. 31, no. 5, Aug. 2021, doi: 10.1007/s11222-021-10038-2.
2159    ///
2160    #[pyo3(signature = (p0, *, bounds=None, method="aies".to_string(), settings=None, observers=None, terminators=None, max_steps=None, debug=false, threads=0))]
2161    #[allow(clippy::too_many_arguments)]
2162    fn mcmc<'py>(
2163        &self,
2164        py: Python<'py>,
2165        p0: Vec<Vec<Float>>,
2166        bounds: Option<Vec<(Option<Float>, Option<Float>)>>,
2167        method: String,
2168        settings: Option<Bound<'py, PyDict>>,
2169        observers: Option<Bound<'_, PyAny>>,
2170        terminators: Option<Bound<'_, PyAny>>,
2171        max_steps: Option<usize>,
2172        debug: bool,
2173        threads: usize,
2174    ) -> PyResult<PyMCMCSummary> {
2175        let full_settings = PyDict::new(py);
2176        if let Some(bounds) = bounds {
2177            full_settings.set_item("bounds", bounds)?;
2178        }
2179        full_settings.set_item("method", method)?;
2180        if let Some(observers) = observers {
2181            full_settings.set_item("observers", observers)?;
2182        }
2183        if let Some(terminators) = terminators {
2184            full_settings.set_item("terminators", terminators)?;
2185        }
2186        if let Some(max_steps) = max_steps {
2187            full_settings.set_item("max_steps", max_steps)?;
2188        }
2189        full_settings.set_item("debug", debug)?;
2190        full_settings.set_item("threads", threads)?;
2191        if let Some(settings) = settings {
2192            full_settings.set_item("settings", settings)?;
2193        }
2194        let result = self.0.mcmc(MCMCSettings::from_pyargs(
2195            &p0.into_iter().map(|p| DVector::from_vec(p)).collect(),
2196            &full_settings,
2197        )?)?;
2198        Ok(PyMCMCSummary(result))
2199    }
2200}
2201
2202/// A stochastic (extended) negative log-likelihood evaluator
2203///
2204/// This evaluator operates on a subset of the data, which may improve performance for large
2205/// datasets at the cost of adding noise to the likelihood.
2206///
2207/// Notes
2208/// -----
2209/// See the `NLL.to_stochastic` method for details.
2210#[cfg(feature = "python")]
2211#[pyclass(name = "StochasticNLL", module = "laddu")]
2212#[derive(Clone)]
2213pub struct PyStochasticNLL(pub StochasticNLL);
2214
2215#[cfg(feature = "python")]
2216#[pymethods]
2217impl PyStochasticNLL {
2218    /// The NLL term containing the underlying model and evaluators
2219    ///
2220    /// Returns
2221    /// -------
2222    /// NLL
2223    ///
2224    #[getter]
2225    fn nll(&self) -> PyNLL {
2226        PyNLL(Box::new(self.0.nll.clone()))
2227    }
2228    /// Minimize the StochasticNLL with respect to the free parameters in the model
2229    ///
2230    /// This method "runs the fit". Given an initial position `p0`, this
2231    /// method performs a minimization over the negative log-likelihood, optimizing the model
2232    /// over the stored signal data and Monte Carlo.
2233    ///
2234    /// Parameters
2235    /// ----------
2236    /// p0 : array_like
2237    ///     The initial parameters at the start of optimization
2238    /// bounds : list of tuple of float or None, optional
2239    ///     Optional bounds on each parameter (use None or an infinity for no bound)
2240    /// method : {'lbfgsb', 'nelder-mead', 'adam', 'pso'}
2241    ///     The minimization algorithm to use
2242    /// settings : dict, optional
2243    ///     Settings for the minimization algorithm (see notes)
2244    /// observers : MinimizerObserver or list of MinimizerObserver, optional
2245    ///     User-defined observers which are called at each step
2246    /// terminators : MinimizerTerminator or list of MinimizerTerminator, optional
2247    ///     User-defined terminators which are called at each step
2248    /// max_steps : int, optional
2249    ///     Set the maximum number of steps
2250    /// debug : bool, default=False
2251    ///     Use a debug observer to print out debugging information at each step
2252    /// threads : int, default=0
2253    ///     The number of threads to use (setting this to 0 will use all available CPUs)
2254    ///
2255    /// Returns
2256    /// -------
2257    /// MinimizationSummary
2258    ///     The status of the minimization algorithm at termination
2259    ///
2260    /// Raises
2261    /// ------
2262    /// Exception
2263    ///     If there was an error building the thread pool
2264    ///
2265    /// Notes
2266    /// -----
2267    /// The `settings` dict is passed to the minimization algorithm as keyword arguments. Each
2268    /// algorithm has different settings:
2269    ///
2270    /// L-BFGS-B
2271    /// ========
2272    /// m : int, default=10
2273    ///     The number of saved corrections to the approximated Hessian.
2274    /// skip_hessian : bool, default=False
2275    ///     If True, the exact Hessian will not be calculated.
2276    /// line_search : dict
2277    ///     Settings for the line search (see next section).
2278    /// eps_f : float, default=`MACH_EPS^(1/2)`
2279    ///     The tolerance for stopping based on the change in function value.
2280    /// eps_g : float, default=`MACH_EPS^(1/3)`
2281    ///     The tolerance for stopping based on the change in function gradient.
2282    /// eps_norm_g : float, default=1e-5
2283    ///     The tolerance for stopping based on the change in the infinity-norm of the function gradient.
2284    ///
2285    /// Line Search
2286    /// ===========
2287    /// method : {"morethuente", "hagerzhang"}
2288    ///     The line search method to use.
2289    /// max_iterations : int, default=100
2290    ///     The maximum number of line search iterations.
2291    /// c1 : float, default=1e-4
2292    ///     The first Wolfe condition constant (More-Thuente only).
2293    /// c2 : float, default=0.9
2294    ///     The second Wolfe condition constant (More-Thuente only).
2295    /// max_zoom : int, default=100
2296    ///     The maximum number of zoom steps (More-Thuente only).
2297    /// delta : float, default=0.1
2298    ///     The first Wolfe condition constant (Hager-Zhang only).
2299    /// sigma : float, default=0.9
2300    ///     The second Wolfe condition constant (Hager-Zhang only).
2301    /// epsilon : float, default=`MACH_EPS^(1/3)`
2302    ///     The tolerance parameter on approximate Wolfe termination (Hager-Zhang only).
2303    /// theta : float, default=0.5
2304    ///     The split ratio for interval updates (defaults to bisection) (Hager-Zhang only).
2305    /// gamma : float, default=0.66
2306    ///     A parameter which determines when a bisection is performed (Hager-Zhang only).
2307    /// max_bisects : int, default=50
2308    ///     The maximum number of allowed bisections (Hager-Zhang only).
2309    ///
2310    /// Adam
2311    /// ====
2312    /// beta_c : float, default=0.9
2313    ///     The slope of the exponential moving average used to terminate the algorithm.
2314    /// eps_loss : float, default=`MACH_EPS^(1/2)`
2315    ///     The minimum change in exponential moving average loss which will increase the patience counter.
2316    /// patience : int, default=1
2317    ///     The number of allowed iterations with no improvement in the loss (according to an exponential moving average) before the algorithm terminates.
2318    ///
2319    /// Nelder-Mead
2320    /// ===========
2321    /// alpha : float, default=1.0
2322    ///     The reflection coefficient.
2323    /// beta : float, default=2.0
2324    ///     The expansion coefficient.
2325    /// gamma : float, default=0.5
2326    ///     The contraction coefficient.
2327    /// delta : float, default=0.5
2328    ///     The shrink coefficient.
2329    /// adaptive : bool, default=False
2330    ///     Use adaptive hyperparameters according to Gao and Han[Gao]_.
2331    /// expansion_method : {"greedyminimization", "greedyexpansion"}
2332    ///     Greedy minimization will favor points which minimize faster, but greedy expansion may explore a space more efficiently. See [Lagarias]_ for details.
2333    /// simplex_construction_method : {"scaledorthogonal", "orthogonal", "custom"}
2334    ///     The method used to generate the initial simplex.
2335    /// orthogonal_multiplier : float, default=1.05
2336    ///     Multiplier used on nonzero coordinates of the initial vertex in simplex generation (scaled orthogonal method).
2337    /// orthogonal_zero_step : float, default=0.00025
2338    ///     Value to use for coordinates of the initial vertex which are zero in simplex generation (scaled orthogonal method).
2339    /// simplex_size : float, default=1.0
2340    ///     The step in each orthogonal direction from the initial vertex in simplex generation (orthogonal method).
2341    /// simplex : list of list of floats
2342    ///     Specify the initial simplex directly. Each entry in the list must be a unique point in the parameter space. The initial vertex is also included, so this argument must specify as many vertices as there are dimensions in the parameter space. This must be specified if simplex_construction_method is set to "custom".
2343    /// f_terminator : {"stddev", "absolute", "amoeba"} or None, default="stddev"
2344    ///     Set the method to terminate the algorithm based on the function values over the simplex. See [Singer]_ for details. Set to None to skip this check.
2345    /// eps_f : float, default=`MACH_EPS^(1/4)`
2346    ///     The tolerance for the f_terminator method.
2347    /// x_terminator : {"singer", "diameter", "higham", "rowan"} or None, default="singer"
2348    ///     Set the method to terminate the algorithm based on the position of simplex vertices. See [Singer]_ for details. Set to None to skip this check.
2349    /// eps_x : float, default=`MACH_EPS^(1/4)`
2350    ///     The tolerance for the x_terminator method.
2351    ///
2352    /// Particle Swarm Optimization (PSO)
2353    /// =================================
2354    /// swarm_position_initializer : {"randominlimits", "latinhypercube", "custom"}
2355    ///     The method used to initialize the swarm position. The "randominlimits" and "latinhypercube" methods require swarm_position_bounds and swarm_size to be specified, and they ignore the initial position given when constructing the swarm (this behavior may change in the future). The "custom" method requires swarm to be specified and does include the initial position.
2356    /// swarm_position_bounds : list of tuple of floats or None
2357    ///     Bounds used when randomly generating a swarm with either the "randominlimits" or "latinhypercube" swarm_position_initializer.
2358    /// swarm_size : int
2359    ///     The number of particles in the swarm when using the "randominlimits" or "latinhypercube" swarm_position_initializer.
2360    /// swarm : list of list of floats
2361    ///     A list of positions of each particle in the swarm. This argument is required when using the "custom" swarm_position_initializer.
2362    /// swarm_topology : {"global", "ring"}
2363    ///     The topology connecting particles in the swarm.
2364    /// swarm_update_method : {"sync", "synchronous", "async", "asynchronous"}
2365    ///     Synchronous updates update positions and targets in separate loops (slower but sometimes more stable) while asynchronous updates them in the same loop (faster but sometimes less stable).
2366    /// swarm_boundary_method : {"inf", "shr"}
2367    ///     The boundary method used for the swarm. "inf" sets infeasable values to +inf while "shr" shrinks the velocity vector to place the particle on the boundary where it would cross.
2368    /// use_transform : bool, default=False
2369    ///     If True, the algorithm will ignore the swarm_boundary_method and instead perform a coordinate transformation on the swarm to ensure the swarm is within bounds.
2370    /// swarm_velocity_bounds : list of tuple of floats or None, optional
2371    ///     Bounds used when randomly generating the initial velocity of each particle in the swarm. If not specified, initial velocities are set to zero.
2372    /// omega : float, default=0.8
2373    ///     The inertial weight.
2374    /// c1 : float, default = 0.1
2375    ///     The cognitive weight.
2376    /// c2 : float, default = 0.1
2377    ///     The social weight.
2378    ///
2379    /// .. rubric:: References
2380    ///
2381    /// .. [Gao] F. Gao and L. Han, “Implementing the Nelder-Mead simplex algorithm with adaptive parameters,” Comput Optim Appl, vol. 51, no. 1, pp. 259–277, May 2010, doi: 10.1007/s10589-010-9329-3.
2382    /// .. [Lagarias] J. C. Lagarias, J. A. Reeds, M. H. Wright, and P. E. Wright, “Convergence Properties of the Nelder--Mead Simplex Method in Low Dimensions,” SIAM J. Optim., vol. 9, no. 1, pp. 112–147, Jan. 1998, doi: 10.1137/s1052623496303470.
2383    /// .. [Singer] S. Singer and S. Singer, “Efficient Implementation of the Nelder–Mead Search Algorithm,” Appl Numer Analy &amp; Comput, vol. 1, no. 2, pp. 524–534, Dec. 2004, doi: 10.1002/anac.200410015.
2384    ///
2385    #[pyo3(signature = (p0, *, bounds=None, method="lbfgsb".to_string(), settings=None, observers=None, terminators=None, max_steps=None, debug=false, threads=0))]
2386    #[allow(clippy::too_many_arguments)]
2387    fn minimize<'py>(
2388        &self,
2389        py: Python<'py>,
2390        p0: Vec<Float>,
2391        bounds: Option<Vec<(Option<Float>, Option<Float>)>>,
2392        method: String,
2393        settings: Option<Bound<'py, PyDict>>,
2394        observers: Option<Bound<'_, PyAny>>,
2395        terminators: Option<Bound<'_, PyAny>>,
2396        max_steps: Option<usize>,
2397        debug: bool,
2398        threads: usize,
2399    ) -> PyResult<PyMinimizationSummary> {
2400        let full_settings = PyDict::new(py);
2401        if let Some(bounds) = bounds {
2402            full_settings.set_item("bounds", bounds)?;
2403        }
2404        full_settings.set_item("method", method)?;
2405        if let Some(observers) = observers {
2406            full_settings.set_item("observers", observers)?;
2407        }
2408        if let Some(terminators) = terminators {
2409            full_settings.set_item("terminators", terminators)?;
2410        }
2411        if let Some(max_steps) = max_steps {
2412            full_settings.set_item("max_steps", max_steps)?;
2413        }
2414        full_settings.set_item("debug", debug)?;
2415        full_settings.set_item("threads", threads)?;
2416        if let Some(settings) = settings {
2417            full_settings.set_item("settings", settings)?;
2418        }
2419        let result = self
2420            .0
2421            .minimize(MinimizationSettings::from_pyargs(&p0, &full_settings)?)?;
2422        Ok(PyMinimizationSummary(result))
2423    }
2424    /// Run an MCMC algorithm on the free parameters of the StochasticNLL's model
2425    ///
2426    /// This method can be used to sample the underlying log-likelihood given an initial
2427    /// position for each walker `p0`.
2428    ///
2429    /// Parameters
2430    /// ----------
2431    /// p0 : array_like
2432    ///     The initial positions of each walker with dimension (n_walkers, n_parameters)
2433    /// bounds : list of tuple of float or None, optional
2434    ///     Optional bounds on each parameter (use None or an infinity for no bound)
2435    /// method : {'aies', 'ess'}
2436    ///     The MCMC algorithm to use
2437    /// settings : dict, optional
2438    ///     Settings for the MCMC algorithm (see notes)
2439    /// observers : MCMCObserver or list of MCMCObserver, optional
2440    ///     User-defined observers which are called at each step
2441    /// terminators : MCMCTerminator or list of MCMCTerminator, optional
2442    ///     User-defined terminators which are called at each step
2443    /// max_steps : int, optional
2444    ///     Set the maximum number of steps
2445    /// debug : bool, default=False
2446    ///     Use a debug observer to print out debugging information at each step
2447    /// threads : int, default=0
2448    ///     The number of threads to use (setting this to 0 will use all available CPUs)
2449    ///
2450    /// Returns
2451    /// -------
2452    /// MCMCSummary
2453    ///     The status of the MCMC algorithm at termination
2454    ///
2455    /// Raises
2456    /// ------
2457    /// Exception
2458    ///     If there was an error building the thread pool
2459    ///
2460    /// Notes
2461    /// -----
2462    /// The `settings` dict is passed to the MCMC algorithm as keyword arguments. Each
2463    /// algorithm has different settings.
2464    ///
2465    /// AIES (Affine-Invariant Ensemble Sampler) [Goodman]_
2466    /// =============================================
2467    /// moves : list of tuple of (str, float) or (str, dict, float), default = [('stretch', {'a': 2.0}, 1.0)]
2468    ///     The list of moves to use. The first element of the tuple is the name of the move
2469    ///     ('stretch' or 'walk') and the last is the frequency that move should be used relative
2470    ///     to the sum of all frequencies given across all moves. An optional middle dictionary
2471    ///     parameter may be provided to specify properties of moves which support it. For the AIES
2472    ///     algorithm, the stretch move may use the 'a' parameter to specify the scaling parameter
2473    ///     (2.0 by default).
2474    ///
2475    /// ESS (Ensemble Slice Sampler) [Karamanis]_
2476    /// =================================
2477    /// moves : list of tuple of (str, float) or (str, dict, float), default = [('differential', 1.0)]
2478    ///     The list of moves to use. The first element of the tuple is the name of the move
2479    ///     ('differential', 'gaussian', or 'global') and the last is the frequency that move
2480    ///     should be used relative to the sum of all frequencies given across all moves. An
2481    ///     optional middle dictionary parameter may be provided to specify properties of moves
2482    ///     which support it. For the ESS algorithm, the global move may use a 'scale' parameter
2483    ///     (1.0 by default) which rescales steps within a local cluster, a 'rescale_cov' parameter
2484    ///     (0.001 by default) which rescales the covariance matrix of clusters, and an
2485    ///     'n_components' parameter (5 by default) which represents the number of mixture
2486    ///     components to use for clustering (should be larger than the expected number of modes).
2487    /// n_adaptive : int, default=0
2488    ///     The number of adaptive moves to perform at the start of sampling
2489    /// max_steps : int, default=10000
2490    ///     The maximum number of expansions/contractions to perform at each step in the algorithm
2491    /// mu : float, default = 1.0
2492    ///     The adaptive scaling parameter (only applies if 'n_adaptive' is greater than zero)
2493    ///
2494    /// .. rubric:: References
2495    ///
2496    /// .. [Goodman] J. Goodman and J. Weare, “Ensemble samplers with affine invariance,” CAMCoS, vol. 5, no. 1, pp. 65–80, Jan. 2010, doi: 10.2140/camcos.2010.5.65.
2497    /// .. [Karamanis] M. Karamanis and F. Beutler, “Ensemble slice sampling,” Stat Comput, vol. 31, no. 5, Aug. 2021, doi: 10.1007/s11222-021-10038-2.
2498    ///
2499    #[pyo3(signature = (p0, *, bounds=None, method="aies".to_string(), settings=None, observers=None, terminators=None, max_steps=None, debug=false, threads=0))]
2500    #[allow(clippy::too_many_arguments)]
2501    fn mcmc<'py>(
2502        &self,
2503        py: Python<'py>,
2504        p0: Vec<Vec<Float>>,
2505        bounds: Option<Vec<(Option<Float>, Option<Float>)>>,
2506        method: String,
2507        settings: Option<Bound<'py, PyDict>>,
2508        observers: Option<Bound<'_, PyAny>>,
2509        terminators: Option<Bound<'_, PyAny>>,
2510        max_steps: Option<usize>,
2511        debug: bool,
2512        threads: usize,
2513    ) -> PyResult<PyMCMCSummary> {
2514        let full_settings = PyDict::new(py);
2515        if let Some(bounds) = bounds {
2516            full_settings.set_item("bounds", bounds)?;
2517        }
2518        full_settings.set_item("method", method)?;
2519        if let Some(observers) = observers {
2520            full_settings.set_item("observers", observers)?;
2521        }
2522        if let Some(terminators) = terminators {
2523            full_settings.set_item("terminators", terminators)?;
2524        }
2525        if let Some(max_steps) = max_steps {
2526            full_settings.set_item("max_steps", max_steps)?;
2527        }
2528        full_settings.set_item("debug", debug)?;
2529        full_settings.set_item("threads", threads)?;
2530        if let Some(settings) = settings {
2531            full_settings.set_item("settings", settings)?;
2532        }
2533        let result = self.0.mcmc(MCMCSettings::from_pyargs(
2534            &p0.into_iter().map(|p| DVector::from_vec(p)).collect(),
2535            &full_settings,
2536        )?)?;
2537        Ok(PyMCMCSummary(result))
2538    }
2539}
2540
2541/// An identifier that can be used like an [`AmplitudeID`](`laddu_core::amplitudes::AmplitudeID`) to combine registered
2542/// [`LikelihoodTerm`]s.
2543#[derive(Clone, Debug)]
2544pub struct LikelihoodID(usize, Option<String>);
2545
2546impl Display for LikelihoodID {
2547    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2548        if let Some(name) = &self.1 {
2549            write!(f, "{}({})", name, self.0)
2550        } else {
2551            write!(f, "({})", self.0)
2552        }
2553    }
2554}
2555
2556/// An object which holds a registered ``LikelihoodTerm``
2557///
2558/// See Also
2559/// --------
2560/// laddu.LikelihoodManager.register
2561///
2562#[cfg(feature = "python")]
2563#[pyclass(name = "LikelihoodID", module = "laddu")]
2564#[derive(Clone)]
2565pub struct PyLikelihoodID(LikelihoodID);
2566
2567#[cfg(feature = "python")]
2568#[pymethods]
2569impl PyLikelihoodID {
2570    fn __add__(&self, other: &Bound<'_, PyAny>) -> PyResult<PyLikelihoodExpression> {
2571        if let Ok(other_aid) = other.extract::<PyRef<PyLikelihoodID>>() {
2572            Ok(PyLikelihoodExpression(self.0.clone() + other_aid.0.clone()))
2573        } else if let Ok(other_expr) = other.extract::<PyLikelihoodExpression>() {
2574            Ok(PyLikelihoodExpression(
2575                self.0.clone() + other_expr.0.clone(),
2576            ))
2577        } else if let Ok(int) = other.extract::<usize>() {
2578            if int == 0 {
2579                Ok(PyLikelihoodExpression(LikelihoodExpression::Term(
2580                    self.0.clone(),
2581                )))
2582            } else {
2583                Err(PyTypeError::new_err(
2584                    "Addition with an integer for this type is only defined for 0",
2585                ))
2586            }
2587        } else {
2588            Err(PyTypeError::new_err("Unsupported operand type for +"))
2589        }
2590    }
2591    fn __radd__(&self, other: &Bound<'_, PyAny>) -> PyResult<PyLikelihoodExpression> {
2592        if let Ok(other_aid) = other.extract::<PyRef<PyLikelihoodID>>() {
2593            Ok(PyLikelihoodExpression(other_aid.0.clone() + self.0.clone()))
2594        } else if let Ok(other_expr) = other.extract::<PyLikelihoodExpression>() {
2595            Ok(PyLikelihoodExpression(
2596                other_expr.0.clone() + self.0.clone(),
2597            ))
2598        } else if let Ok(int) = other.extract::<usize>() {
2599            if int == 0 {
2600                Ok(PyLikelihoodExpression(LikelihoodExpression::Term(
2601                    self.0.clone(),
2602                )))
2603            } else {
2604                Err(PyTypeError::new_err(
2605                    "Addition with an integer for this type is only defined for 0",
2606                ))
2607            }
2608        } else {
2609            Err(PyTypeError::new_err("Unsupported operand type for +"))
2610        }
2611    }
2612    fn __mul__(&self, other: &Bound<'_, PyAny>) -> PyResult<PyLikelihoodExpression> {
2613        if let Ok(other_aid) = other.extract::<PyRef<PyLikelihoodID>>() {
2614            Ok(PyLikelihoodExpression(self.0.clone() * other_aid.0.clone()))
2615        } else if let Ok(other_expr) = other.extract::<PyLikelihoodExpression>() {
2616            Ok(PyLikelihoodExpression(
2617                self.0.clone() * other_expr.0.clone(),
2618            ))
2619        } else {
2620            Err(PyTypeError::new_err("Unsupported operand type for *"))
2621        }
2622    }
2623    fn __rmul__(&self, other: &Bound<'_, PyAny>) -> PyResult<PyLikelihoodExpression> {
2624        if let Ok(other_aid) = other.extract::<PyRef<PyLikelihoodID>>() {
2625            Ok(PyLikelihoodExpression(other_aid.0.clone() * self.0.clone()))
2626        } else if let Ok(other_expr) = other.extract::<PyLikelihoodExpression>() {
2627            Ok(PyLikelihoodExpression(
2628                other_expr.0.clone() * self.0.clone(),
2629            ))
2630        } else {
2631            Err(PyTypeError::new_err("Unsupported operand type for *"))
2632        }
2633    }
2634    fn __str__(&self) -> String {
2635        format!("{}", self.0)
2636    }
2637    fn __repr__(&self) -> String {
2638        format!("{:?}", self.0)
2639    }
2640}
2641
2642/// A [`Manager`](`laddu_core::Manager`) but for [`LikelihoodTerm`]s.
2643#[derive(Default, Clone)]
2644pub struct LikelihoodManager {
2645    terms: Vec<Box<dyn LikelihoodTerm>>,
2646    param_name_to_index: HashMap<String, usize>,
2647    param_names: Vec<String>,
2648    param_layouts: Vec<Vec<usize>>,
2649    param_counts: Vec<usize>,
2650}
2651
2652impl LikelihoodManager {
2653    /// Register a [`LikelihoodTerm`] to get a [`LikelihoodID`] which can be combined with others
2654    /// to form [`LikelihoodExpression`]s which can be minimized.
2655    pub fn register(&mut self, term: Box<dyn LikelihoodTerm>) -> LikelihoodID {
2656        let term_idx = self.terms.len();
2657        for param_name in term.parameters() {
2658            if !self.param_name_to_index.contains_key(&param_name) {
2659                self.param_name_to_index
2660                    .insert(param_name.clone(), self.param_name_to_index.len());
2661                self.param_names.push(param_name.clone());
2662            }
2663        }
2664        let param_layout: Vec<usize> = term
2665            .parameters()
2666            .iter()
2667            .map(|name| self.param_name_to_index[name])
2668            .collect();
2669        let param_count = term.parameters().len();
2670        self.param_layouts.push(param_layout);
2671        self.param_counts.push(param_count);
2672        self.terms.push(term.clone());
2673
2674        LikelihoodID(term_idx, None)
2675    }
2676
2677    /// Register (and name) a [`LikelihoodTerm`] to get a [`LikelihoodID`] which can be combined with others
2678    /// to form [`LikelihoodExpression`]s which can be minimized.
2679    pub fn register_with_name<T: AsRef<str>>(
2680        &mut self,
2681        term: Box<dyn LikelihoodTerm>,
2682        name: T,
2683    ) -> LikelihoodID {
2684        let term_idx = self.terms.len();
2685        for param_name in term.parameters() {
2686            if !self.param_name_to_index.contains_key(&param_name) {
2687                self.param_name_to_index
2688                    .insert(param_name.clone(), self.param_name_to_index.len());
2689                self.param_names.push(param_name.clone());
2690            }
2691        }
2692        let param_layout: Vec<usize> = term
2693            .parameters()
2694            .iter()
2695            .map(|name| self.param_name_to_index[name])
2696            .collect();
2697        let param_count = term.parameters().len();
2698        self.param_layouts.push(param_layout);
2699        self.param_counts.push(param_count);
2700        self.terms.push(term.clone());
2701
2702        LikelihoodID(term_idx, Some(name.as_ref().to_string().clone()))
2703    }
2704
2705    /// Return all of the parameter names of registered [`LikelihoodTerm`]s in order. This only
2706    /// returns the unique names in the order they should be input when evaluated with a
2707    /// [`LikelihoodEvaluator`].
2708    pub fn parameters(&self) -> Vec<String> {
2709        self.param_names.clone()
2710    }
2711
2712    /// Load a [`LikelihoodExpression`] to generate a [`LikelihoodEvaluator`] that can be
2713    /// minimized.
2714    pub fn load(&self, likelihood_expression: &LikelihoodExpression) -> LikelihoodEvaluator {
2715        LikelihoodEvaluator {
2716            likelihood_manager: self.clone(),
2717            likelihood_expression: likelihood_expression.clone(),
2718        }
2719    }
2720}
2721
2722/// A class which can be used to register LikelihoodTerms and store precalculated data
2723///
2724#[cfg(feature = "python")]
2725#[pyclass(name = "LikelihoodManager", module = "laddu")]
2726#[derive(Clone)]
2727pub struct PyLikelihoodManager(LikelihoodManager);
2728
2729#[cfg(feature = "python")]
2730#[pymethods]
2731impl PyLikelihoodManager {
2732    #[new]
2733    fn new() -> Self {
2734        Self(LikelihoodManager::default())
2735    }
2736    /// Register a LikelihoodTerm with the LikelihoodManager
2737    ///
2738    /// Parameters
2739    /// ----------
2740    /// term : LikelihoodTerm
2741    ///     The LikelihoodTerm to register
2742    ///
2743    /// Returns
2744    /// -------
2745    /// LikelihoodID
2746    ///     A reference to the registered ``likelihood`` that can be used to form complex
2747    ///     LikelihoodExpressions
2748    ///
2749    #[pyo3(signature = (likelihood_term, *, name = None))]
2750    fn register(
2751        &mut self,
2752        likelihood_term: &PyLikelihoodTerm,
2753        name: Option<String>,
2754    ) -> PyLikelihoodID {
2755        if let Some(name) = name {
2756            PyLikelihoodID(self.0.register_with_name(likelihood_term.0.clone(), name))
2757        } else {
2758            PyLikelihoodID(self.0.register(likelihood_term.0.clone()))
2759        }
2760    }
2761    /// The free parameters used by all terms in the LikelihoodManager
2762    ///
2763    /// Returns
2764    /// -------
2765    /// parameters : list of str
2766    ///     The list of parameter names
2767    ///
2768    fn parameters(&self) -> Vec<String> {
2769        self.0.parameters()
2770    }
2771    /// Load a LikelihoodExpression by precalculating each term over their internal Datasets
2772    ///
2773    /// Parameters
2774    /// ----------
2775    /// likelihood_expression : LikelihoodExpression
2776    ///     The expression to use in precalculation
2777    ///
2778    /// Returns
2779    /// -------
2780    /// LikelihoodEvaluator
2781    ///     An object that can be used to evaluate the `likelihood_expression` over all managed
2782    ///     terms
2783    ///
2784    /// Notes
2785    /// -----
2786    /// While the given `likelihood_expression` will be the one evaluated in the end, all registered
2787    /// Amplitudes will be loaded, and all of their parameters will be included in the final
2788    /// expression. These parameters will have no effect on evaluation, but they must be
2789    /// included in function calls.
2790    ///
2791    /// See Also
2792    /// --------
2793    /// LikelihoodManager.parameters
2794    ///
2795    fn load(&self, likelihood_expression: &PyLikelihoodExpression) -> PyLikelihoodEvaluator {
2796        PyLikelihoodEvaluator(self.0.load(&likelihood_expression.0))
2797    }
2798}
2799
2800#[derive(Debug)]
2801struct LikelihoodValues(Vec<Float>);
2802
2803#[derive(Debug)]
2804struct LikelihoodGradients(Vec<DVector<Float>>);
2805
2806/// A combination of [`LikelihoodTerm`]s as well as sums and products of them.
2807#[derive(Clone, Default)]
2808pub enum LikelihoodExpression {
2809    /// A expression equal to zero.
2810    #[default]
2811    Zero,
2812    /// A expression equal to one.
2813    One,
2814    /// A registered [`LikelihoodTerm`] referenced by an [`LikelihoodID`].
2815    Term(LikelihoodID),
2816    /// The sum of two [`LikelihoodExpression`]s.
2817    Add(Box<LikelihoodExpression>, Box<LikelihoodExpression>),
2818    /// The product of two [`LikelihoodExpression`]s.
2819    Mul(Box<LikelihoodExpression>, Box<LikelihoodExpression>),
2820}
2821
2822impl Debug for LikelihoodExpression {
2823    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2824        self.write_tree(f, "", "", "")
2825    }
2826}
2827
2828impl Display for LikelihoodExpression {
2829    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2830        write!(f, "{:?}", self)
2831    }
2832}
2833
2834impl LikelihoodExpression {
2835    fn evaluate(&self, likelihood_values: &LikelihoodValues) -> Float {
2836        match self {
2837            LikelihoodExpression::Zero => 0.0,
2838            LikelihoodExpression::One => 1.0,
2839            LikelihoodExpression::Term(lid) => likelihood_values.0[lid.0],
2840            LikelihoodExpression::Add(a, b) => {
2841                a.evaluate(likelihood_values) + b.evaluate(likelihood_values)
2842            }
2843            LikelihoodExpression::Mul(a, b) => {
2844                a.evaluate(likelihood_values) * b.evaluate(likelihood_values)
2845            }
2846        }
2847    }
2848    fn evaluate_gradient(
2849        &self,
2850        likelihood_values: &LikelihoodValues,
2851        likelihood_gradients: &LikelihoodGradients,
2852    ) -> DVector<Float> {
2853        match self {
2854            LikelihoodExpression::Zero => DVector::zeros(0),
2855            LikelihoodExpression::One => DVector::zeros(0),
2856            LikelihoodExpression::Term(lid) => likelihood_gradients.0[lid.0].clone(),
2857            LikelihoodExpression::Add(a, b) => {
2858                a.evaluate_gradient(likelihood_values, likelihood_gradients)
2859                    + b.evaluate_gradient(likelihood_values, likelihood_gradients)
2860            }
2861            LikelihoodExpression::Mul(a, b) => {
2862                a.evaluate_gradient(likelihood_values, likelihood_gradients)
2863                    * b.evaluate(likelihood_values)
2864                    + b.evaluate_gradient(likelihood_values, likelihood_gradients)
2865                        * a.evaluate(likelihood_values)
2866            }
2867        }
2868    }
2869    /// Credit to Daniel Janus: <https://blog.danieljanus.pl/2023/07/20/iterating-trees/>
2870    fn write_tree(
2871        &self,
2872        f: &mut std::fmt::Formatter<'_>,
2873        parent_prefix: &str,
2874        immediate_prefix: &str,
2875        parent_suffix: &str,
2876    ) -> std::fmt::Result {
2877        let display_string = match self {
2878            Self::Zero => "0".to_string(),
2879            Self::One => "1".to_string(),
2880            Self::Term(lid) => {
2881                format!("{}", lid.0)
2882            }
2883            Self::Add(_, _) => "+".to_string(),
2884            Self::Mul(_, _) => "*".to_string(),
2885        };
2886        writeln!(f, "{}{}{}", parent_prefix, immediate_prefix, display_string)?;
2887        match self {
2888            Self::Term(_) | Self::Zero | Self::One => {}
2889            Self::Add(a, b) | Self::Mul(a, b) => {
2890                let terms = [a, b];
2891                let mut it = terms.iter().peekable();
2892                let child_prefix = format!("{}{}", parent_prefix, parent_suffix);
2893                while let Some(child) = it.next() {
2894                    match it.peek() {
2895                        Some(_) => child.write_tree(f, &child_prefix, "├─ ", "│  "),
2896                        None => child.write_tree(f, &child_prefix, "└─ ", "   "),
2897                    }?;
2898                }
2899            }
2900        }
2901        Ok(())
2902    }
2903}
2904
2905impl_op_ex!(+ |a: &LikelihoodExpression, b: &LikelihoodExpression| -> LikelihoodExpression { LikelihoodExpression::Add(Box::new(a.clone()), Box::new(b.clone()))});
2906impl_op_ex!(
2907    *|a: &LikelihoodExpression, b: &LikelihoodExpression| -> LikelihoodExpression {
2908        LikelihoodExpression::Mul(Box::new(a.clone()), Box::new(b.clone()))
2909    }
2910);
2911impl_op_ex_commutative!(+ |a: &LikelihoodID, b: &LikelihoodExpression| -> LikelihoodExpression { LikelihoodExpression::Add(Box::new(LikelihoodExpression::Term(a.clone())), Box::new(b.clone()))});
2912impl_op_ex_commutative!(
2913    *|a: &LikelihoodID, b: &LikelihoodExpression| -> LikelihoodExpression {
2914        LikelihoodExpression::Mul(
2915            Box::new(LikelihoodExpression::Term(a.clone())),
2916            Box::new(b.clone()),
2917        )
2918    }
2919);
2920impl_op_ex!(+ |a: &LikelihoodID, b: &LikelihoodID| -> LikelihoodExpression { LikelihoodExpression::Add(Box::new(LikelihoodExpression::Term(a.clone())), Box::new(LikelihoodExpression::Term(b.clone())))});
2921impl_op_ex!(
2922    *|a: &LikelihoodID, b: &LikelihoodID| -> LikelihoodExpression {
2923        LikelihoodExpression::Mul(
2924            Box::new(LikelihoodExpression::Term(a.clone())),
2925            Box::new(LikelihoodExpression::Term(b.clone())),
2926        )
2927    }
2928);
2929
2930/// A mathematical expression formed from LikelihoodIDs
2931///
2932#[cfg(feature = "python")]
2933#[pyclass(name = "LikelihoodExpression", module = "laddu")]
2934#[derive(Clone)]
2935pub struct PyLikelihoodExpression(LikelihoodExpression);
2936
2937/// A convenience method to sum sequences of LikelihoodExpressions
2938///
2939#[cfg(feature = "python")]
2940#[pyfunction(name = "likelihood_sum")]
2941pub fn py_likelihood_sum(terms: Vec<Bound<'_, PyAny>>) -> PyResult<PyLikelihoodExpression> {
2942    if terms.is_empty() {
2943        return Ok(PyLikelihoodExpression(LikelihoodExpression::Zero));
2944    }
2945    if terms.len() == 1 {
2946        let term = &terms[0];
2947        if let Ok(expression) = term.extract::<PyLikelihoodExpression>() {
2948            return Ok(expression);
2949        }
2950        if let Ok(py_amplitude_id) = term.extract::<PyLikelihoodID>() {
2951            return Ok(PyLikelihoodExpression(LikelihoodExpression::Term(
2952                py_amplitude_id.0,
2953            )));
2954        }
2955        return Err(PyTypeError::new_err(
2956            "Item is neither a PyLikelihoodExpression nor a PyLikelihoodID",
2957        ));
2958    }
2959    let mut iter = terms.iter();
2960    let Some(first_term) = iter.next() else {
2961        return Ok(PyLikelihoodExpression(LikelihoodExpression::Zero));
2962    };
2963    if let Ok(first_expression) = first_term.extract::<PyLikelihoodExpression>() {
2964        let mut summation = first_expression.clone();
2965        for term in iter {
2966            summation = summation.__add__(term)?;
2967        }
2968        return Ok(summation);
2969    }
2970    if let Ok(first_likelihood_id) = first_term.extract::<PyLikelihoodID>() {
2971        let mut summation =
2972            PyLikelihoodExpression(LikelihoodExpression::Term(first_likelihood_id.0));
2973        for term in iter {
2974            summation = summation.__add__(term)?;
2975        }
2976        return Ok(summation);
2977    }
2978    Err(PyTypeError::new_err(
2979        "Elements must be PyLikelihoodExpression or PyLikelihoodID",
2980    ))
2981}
2982
2983/// A convenience method to multiply sequences of LikelihoodExpressions
2984///
2985#[cfg(feature = "python")]
2986#[pyfunction(name = "likelihood_product")]
2987pub fn py_likelihood_product(terms: Vec<Bound<'_, PyAny>>) -> PyResult<PyLikelihoodExpression> {
2988    if terms.is_empty() {
2989        return Ok(PyLikelihoodExpression(LikelihoodExpression::One));
2990    }
2991    if terms.len() == 1 {
2992        let term = &terms[0];
2993        if let Ok(expression) = term.extract::<PyLikelihoodExpression>() {
2994            return Ok(expression);
2995        }
2996        if let Ok(py_amplitude_id) = term.extract::<PyLikelihoodID>() {
2997            return Ok(PyLikelihoodExpression(LikelihoodExpression::Term(
2998                py_amplitude_id.0,
2999            )));
3000        }
3001        return Err(PyTypeError::new_err(
3002            "Item is neither a PyLikelihoodExpression nor a PyLikelihoodID",
3003        ));
3004    }
3005    let mut iter = terms.iter();
3006    let Some(first_term) = iter.next() else {
3007        return Ok(PyLikelihoodExpression(LikelihoodExpression::One));
3008    };
3009    if let Ok(first_expression) = first_term.extract::<PyLikelihoodExpression>() {
3010        let mut product = first_expression.clone();
3011        for term in iter {
3012            product = product.__mul__(term)?;
3013        }
3014        return Ok(product);
3015    }
3016    if let Ok(first_likelihood_id) = first_term.extract::<PyLikelihoodID>() {
3017        let mut product = PyLikelihoodExpression(LikelihoodExpression::Term(first_likelihood_id.0));
3018        for term in iter {
3019            product = product.__mul__(term)?;
3020        }
3021        return Ok(product);
3022    }
3023    Err(PyTypeError::new_err(
3024        "Elements must be PyLikelihoodExpression or PyLikelihoodID",
3025    ))
3026}
3027
3028/// A convenience class representing a zero-valued LikelihoodExpression
3029///
3030#[cfg(feature = "python")]
3031#[pyfunction(name = "LikelihoodZero")]
3032pub fn py_likelihood_zero() -> PyLikelihoodExpression {
3033    PyLikelihoodExpression(LikelihoodExpression::Zero)
3034}
3035
3036/// A convenience class representing a unit-valued LikelihoodExpression
3037///
3038#[cfg(feature = "python")]
3039#[pyfunction(name = "LikelihoodOne")]
3040pub fn py_likelihood_one() -> PyLikelihoodExpression {
3041    PyLikelihoodExpression(LikelihoodExpression::One)
3042}
3043
3044#[cfg(feature = "python")]
3045#[pymethods]
3046impl PyLikelihoodExpression {
3047    fn __add__(&self, other: &Bound<'_, PyAny>) -> PyResult<PyLikelihoodExpression> {
3048        if let Ok(other_aid) = other.extract::<PyRef<PyLikelihoodID>>() {
3049            Ok(PyLikelihoodExpression(self.0.clone() + other_aid.0.clone()))
3050        } else if let Ok(other_expr) = other.extract::<PyLikelihoodExpression>() {
3051            Ok(PyLikelihoodExpression(
3052                self.0.clone() + other_expr.0.clone(),
3053            ))
3054        } else if let Ok(int) = other.extract::<usize>() {
3055            if int == 0 {
3056                Ok(PyLikelihoodExpression(self.0.clone()))
3057            } else {
3058                Err(PyTypeError::new_err(
3059                    "Addition with an integer for this type is only defined for 0",
3060                ))
3061            }
3062        } else {
3063            Err(PyTypeError::new_err("Unsupported operand type for +"))
3064        }
3065    }
3066    fn __radd__(&self, other: &Bound<'_, PyAny>) -> PyResult<PyLikelihoodExpression> {
3067        if let Ok(other_aid) = other.extract::<PyRef<PyLikelihoodID>>() {
3068            Ok(PyLikelihoodExpression(other_aid.0.clone() + self.0.clone()))
3069        } else if let Ok(other_expr) = other.extract::<PyLikelihoodExpression>() {
3070            Ok(PyLikelihoodExpression(
3071                other_expr.0.clone() + self.0.clone(),
3072            ))
3073        } else if let Ok(int) = other.extract::<usize>() {
3074            if int == 0 {
3075                Ok(PyLikelihoodExpression(self.0.clone()))
3076            } else {
3077                Err(PyTypeError::new_err(
3078                    "Addition with an integer for this type is only defined for 0",
3079                ))
3080            }
3081        } else {
3082            Err(PyTypeError::new_err("Unsupported operand type for +"))
3083        }
3084    }
3085    fn __mul__(&self, other: &Bound<'_, PyAny>) -> PyResult<PyLikelihoodExpression> {
3086        if let Ok(other_aid) = other.extract::<PyRef<PyLikelihoodID>>() {
3087            Ok(PyLikelihoodExpression(self.0.clone() * other_aid.0.clone()))
3088        } else if let Ok(other_expr) = other.extract::<PyLikelihoodExpression>() {
3089            Ok(PyLikelihoodExpression(
3090                self.0.clone() * other_expr.0.clone(),
3091            ))
3092        } else {
3093            Err(PyTypeError::new_err("Unsupported operand type for *"))
3094        }
3095    }
3096    fn __rmul__(&self, other: &Bound<'_, PyAny>) -> PyResult<PyLikelihoodExpression> {
3097        if let Ok(other_aid) = other.extract::<PyRef<PyLikelihoodID>>() {
3098            Ok(PyLikelihoodExpression(self.0.clone() * other_aid.0.clone()))
3099        } else if let Ok(other_expr) = other.extract::<PyLikelihoodExpression>() {
3100            Ok(PyLikelihoodExpression(
3101                self.0.clone() * other_expr.0.clone(),
3102            ))
3103        } else {
3104            Err(PyTypeError::new_err("Unsupported operand type for *"))
3105        }
3106    }
3107    fn __str__(&self) -> String {
3108        format!("{}", self.0)
3109    }
3110    fn __repr__(&self) -> String {
3111        format!("{:?}", self.0)
3112    }
3113}
3114
3115/// A structure to evaluate and minimize combinations of [`LikelihoodTerm`]s.
3116#[derive(Clone)]
3117pub struct LikelihoodEvaluator {
3118    likelihood_manager: LikelihoodManager,
3119    likelihood_expression: LikelihoodExpression,
3120}
3121
3122impl LikelihoodTerm for LikelihoodEvaluator {
3123    fn update(&self) {
3124        self.likelihood_manager
3125            .terms
3126            .iter()
3127            .for_each(|term| term.update())
3128    }
3129    /// The parameter names used in [`LikelihoodEvaluator::evaluate`]'s input in order.
3130    fn parameters(&self) -> Vec<String> {
3131        self.likelihood_manager.parameters()
3132    }
3133    /// A function that can be called to evaluate the sum/product of the [`LikelihoodTerm`]s
3134    /// contained by this [`LikelihoodEvaluator`].
3135    fn evaluate(&self, parameters: &[Float]) -> Float {
3136        let mut param_buffers: Vec<Vec<Float>> = self
3137            .likelihood_manager
3138            .param_counts
3139            .iter()
3140            .map(|&count| vec![0.0; count])
3141            .collect();
3142        for (layout, buffer) in self
3143            .likelihood_manager
3144            .param_layouts
3145            .iter()
3146            .zip(param_buffers.iter_mut())
3147        {
3148            for (buffer_idx, &param_idx) in layout.iter().enumerate() {
3149                buffer[buffer_idx] = parameters[param_idx];
3150            }
3151        }
3152        let likelihood_values = LikelihoodValues(
3153            self.likelihood_manager
3154                .terms
3155                .iter()
3156                .zip(param_buffers.iter())
3157                .map(|(term, buffer)| term.evaluate(buffer))
3158                .collect(),
3159        );
3160        self.likelihood_expression.evaluate(&likelihood_values)
3161    }
3162
3163    /// Evaluate the gradient of the stored [`LikelihoodExpression`] over the events in the [`Dataset`]
3164    /// stored by the [`LikelihoodEvaluator`] with the given values for free parameters.
3165    fn evaluate_gradient(&self, parameters: &[Float]) -> DVector<Float> {
3166        let mut param_buffers: Vec<Vec<Float>> = self
3167            .likelihood_manager
3168            .param_counts
3169            .iter()
3170            .map(|&count| vec![0.0; count])
3171            .collect();
3172        for (layout, buffer) in self
3173            .likelihood_manager
3174            .param_layouts
3175            .iter()
3176            .zip(param_buffers.iter_mut())
3177        {
3178            for (buffer_idx, &param_idx) in layout.iter().enumerate() {
3179                buffer[buffer_idx] = parameters[param_idx];
3180            }
3181        }
3182        let likelihood_values = LikelihoodValues(
3183            self.likelihood_manager
3184                .terms
3185                .iter()
3186                .zip(param_buffers.iter())
3187                .map(|(term, buffer)| term.evaluate(buffer))
3188                .collect(),
3189        );
3190        let mut gradient_buffers: Vec<DVector<Float>> = (0..self.likelihood_manager.terms.len())
3191            .map(|_| DVector::zeros(self.likelihood_manager.param_names.len()))
3192            .collect();
3193        for (((term, param_buffer), gradient_buffer), layout) in self
3194            .likelihood_manager
3195            .terms
3196            .iter()
3197            .zip(param_buffers.iter())
3198            .zip(gradient_buffers.iter_mut())
3199            .zip(self.likelihood_manager.param_layouts.iter())
3200        {
3201            let term_gradient = term.evaluate_gradient(param_buffer); // This has a local layout
3202            for (term_idx, &buffer_idx) in layout.iter().enumerate() {
3203                gradient_buffer[buffer_idx] = term_gradient[term_idx] // This has a global layout
3204            }
3205        }
3206        let likelihood_gradients = LikelihoodGradients(gradient_buffers);
3207        self.likelihood_expression
3208            .evaluate_gradient(&likelihood_values, &likelihood_gradients)
3209    }
3210}
3211
3212/// A class which can be used to evaluate a collection of LikelihoodTerms managed by a
3213/// LikelihoodManager
3214///
3215#[cfg(feature = "python")]
3216#[pyclass(name = "LikelihoodEvaluator", module = "laddu")]
3217pub struct PyLikelihoodEvaluator(LikelihoodEvaluator);
3218
3219#[cfg(feature = "python")]
3220#[pymethods]
3221impl PyLikelihoodEvaluator {
3222    /// A list of the names of the free parameters across all terms in all models
3223    ///
3224    /// Returns
3225    /// -------
3226    /// parameters : list of str
3227    ///
3228    #[getter]
3229    fn parameters(&self) -> Vec<String> {
3230        self.0.parameters()
3231    }
3232    /// Evaluate the sum of all terms in the evaluator
3233    ///
3234    /// Parameters
3235    /// ----------
3236    /// parameters : list of float
3237    ///     The values to use for the free parameters
3238    /// threads : int, optional
3239    ///     The number of threads to use (setting this to None will use all available CPUs)
3240    ///
3241    /// Returns
3242    /// -------
3243    /// result : float
3244    ///     The total negative log-likelihood summed over all terms
3245    ///
3246    /// Raises
3247    /// ------
3248    /// Exception
3249    ///     If there was an error building the thread pool
3250    ///
3251    #[pyo3(signature = (parameters, *, threads=None))]
3252    fn evaluate(&self, parameters: Vec<Float>, threads: Option<usize>) -> PyResult<Float> {
3253        #[cfg(feature = "rayon")]
3254        {
3255            Ok(ThreadPoolBuilder::new()
3256                .num_threads(threads.unwrap_or(0))
3257                .build()
3258                .map_err(LadduError::from)?
3259                .install(|| self.0.evaluate(&parameters)))
3260        }
3261        #[cfg(not(feature = "rayon"))]
3262        {
3263            Ok(self.0.evaluate(&parameters))
3264        }
3265    }
3266    /// Evaluate the gradient of the sum of all terms in the evaluator
3267    ///
3268    /// Parameters
3269    /// ----------
3270    /// parameters : list of float
3271    ///     The values to use for the free parameters
3272    /// threads : int, optional
3273    ///     The number of threads to use (setting this to None will use all available CPUs)
3274    ///
3275    /// Returns
3276    /// -------
3277    /// result : array_like
3278    ///     A ``numpy`` array of representing the gradient of the sum of all terms in the
3279    ///     evaluator
3280    ///
3281    /// Raises
3282    /// ------
3283    /// Exception
3284    ///     If there was an error building the thread pool or problem creating the resulting
3285    ///     ``numpy`` array
3286    ///
3287    #[pyo3(signature = (parameters, *, threads=None))]
3288    fn evaluate_gradient<'py>(
3289        &self,
3290        py: Python<'py>,
3291        parameters: Vec<Float>,
3292        threads: Option<usize>,
3293    ) -> PyResult<Bound<'py, PyArray1<Float>>> {
3294        #[cfg(feature = "rayon")]
3295        {
3296            Ok(PyArray1::from_slice(
3297                py,
3298                ThreadPoolBuilder::new()
3299                    .num_threads(threads.unwrap_or(0))
3300                    .build()
3301                    .map_err(LadduError::from)?
3302                    .install(|| self.0.evaluate_gradient(&parameters))
3303                    .as_slice(),
3304            ))
3305        }
3306        #[cfg(not(feature = "rayon"))]
3307        {
3308            Ok(PyArray1::from_slice(
3309                py,
3310                self.0.evaluate_gradient(&parameters).as_slice(),
3311            ))
3312        }
3313    }
3314    /// Minimize the LikelihoodTerm with respect to the free parameters in the model
3315    ///
3316    /// This method "runs the fit". Given an initial position `p0`, this
3317    /// method performs a minimization over the likelihood term, optimizing the model
3318    /// over the stored signal data and Monte Carlo.
3319    ///
3320    /// Parameters
3321    /// ----------
3322    /// p0 : array_like
3323    ///     The initial parameters at the start of optimization
3324    /// bounds : list of tuple of float or None, optional
3325    ///     Optional bounds on each parameter (use None or an infinity for no bound)
3326    /// method : {'lbfgsb', 'nelder-mead', 'adam', 'pso'}
3327    ///     The minimization algorithm to use
3328    /// settings : dict, optional
3329    ///     Settings for the minimization algorithm (see notes)
3330    /// observers : MinimizerObserver or list of MinimizerObserver, optional
3331    ///     User-defined observers which are called at each step
3332    /// terminators : MinimizerTerminator or list of MinimizerTerminator, optional
3333    ///     User-defined terminators which are called at each step
3334    /// max_steps : int, optional
3335    ///     Set the maximum number of steps
3336    /// debug : bool, default=False
3337    ///     Use a debug observer to print out debugging information at each step
3338    /// threads : int, default=0
3339    ///     The number of threads to use (setting this to 0 will use all available CPUs)
3340    ///
3341    /// Returns
3342    /// -------
3343    /// MinimizationSummary
3344    ///     The status of the minimization algorithm at termination
3345    ///
3346    /// Raises
3347    /// ------
3348    /// Exception
3349    ///     If there was an error building the thread pool
3350    ///
3351    /// Notes
3352    /// -----
3353    /// The `settings` dict is passed to the minimization algorithm as keyword arguments. Each
3354    /// algorithm has different settings:
3355    ///
3356    /// L-BFGS-B
3357    /// ========
3358    /// m : int, default=10
3359    ///     The number of saved corrections to the approximated Hessian.
3360    /// skip_hessian : bool, default=False
3361    ///     If True, the exact Hessian will not be calculated.
3362    /// line_search : dict
3363    ///     Settings for the line search (see next section).
3364    /// eps_f : float,  default=`MACH_EPS^(1/2)`
3365    ///     The tolerance for stopping based on the change in function value.
3366    /// eps_g : float, default=`MACH_EPS^(1/3)`
3367    ///     The tolerance for stopping based on the change in function gradient.
3368    /// eps_norm_g : float, default=1e-5
3369    ///     The tolerance for stopping based on the change in the infinity-norm of the function gradient.
3370    ///
3371    /// Line Search
3372    /// ===========
3373    /// method : {"morethuente", "hagerzhang"}
3374    ///     The line search method to use.
3375    /// max_iterations : int, default=100
3376    ///     The maximum number of line search iterations.
3377    /// c1 : float, default=1e-4
3378    ///     The first Wolfe condition constant (More-Thuente only).
3379    /// c2 : float, default=0.9
3380    ///     The second Wolfe condition constant (More-Thuente only).
3381    /// max_zoom : int, default=100
3382    ///     The maximum number of zoom steps (More-Thuente only).
3383    /// delta : float, default=0.1
3384    ///     The first Wolfe condition constant (Hager-Zhang only).
3385    /// sigma : float, default=0.9
3386    ///     The second Wolfe condition constant (Hager-Zhang only).
3387    /// epsilon : float, default=`MACH_EPS^(1/3)`
3388    ///     The tolerance parameter on approximate Wolfe termination (Hager-Zhang only).
3389    /// theta : float, default=0.5
3390    ///     The split ratio for interval updates (defaults to bisection) (Hager-Zhang only).
3391    /// gamma : float, default=0.66
3392    ///     A parameter which determines when a bisection is performed (Hager-Zhang only).
3393    /// max_bisects : int, default=50
3394    ///     The maximum number of allowed bisections (Hager-Zhang only).
3395    ///
3396    /// Adam
3397    /// ====
3398    /// beta_c : float, default=0.9
3399    ///     The slope of the exponential moving average used to terminate the algorithm.
3400    /// eps_loss : float, default=`MACH_EPS^(1/2)`
3401    ///     The minimum change in exponential moving average loss which will increase the patience counter.
3402    /// patience : int, default=1
3403    ///     The number of allowed iterations with no improvement in the loss (according to an exponential moving average) before the algorithm terminates.
3404    ///
3405    /// Nelder-Mead
3406    /// ===========
3407    /// alpha : float, default=1.0
3408    ///     The reflection coefficient.
3409    /// beta : float, default=2.0
3410    ///     The expansion coefficient.
3411    /// gamma : float, default=0.5
3412    ///     The contraction coefficient.
3413    /// delta : float, default=0.5
3414    ///     The shrink coefficient.
3415    /// adaptive : bool, default=False
3416    ///     Use adaptive hyperparameters according to Gao and Han[Gao]_.
3417    /// expansion_method : {"greedyminimization", "greedyexpansion"}
3418    ///     Greedy minimization will favor points which minimize faster, but greedy expansion may explore a space more efficiently. See [Lagarias]_ for details.
3419    /// simplex_construction_method : {"scaledorthogonal", "orthogonal", "custom"}
3420    ///     The method used to generate the initial simplex.
3421    /// orthogonal_multiplier : float, default=1.05
3422    ///     Multiplier used on nonzero coordinates of the initial vertex in simplex generation (scaled orthogonal method).
3423    /// orthogonal_zero_step : float, default=0.00025
3424    ///     Value to use for coordinates of the initial vertex which are zero in simplex generation (scaled orthogonal method).
3425    /// simplex_size : float, default=1.0
3426    ///     The step in each orthogonal direction from the initial vertex in simplex generation (orthogonal method).
3427    /// simplex : list of list of floats
3428    ///     Specify the initial simplex directly. Each entry in the list must be a unique point in the parameter space. The initial vertex is also included, so this argument must specify as many vertices as there are dimensions in the parameter space. This must be specified if simplex_construction_method is set to "custom".
3429    /// f_terminator : {"stddev", "absolute", "amoeba"} or None, default="stddev"
3430    ///     Set the method to terminate the algorithm based on the function values over the simplex. See [Singer]_ for details. Set to None to skip this check.
3431    /// eps_f : float, default=`MACH_EPS^(1/4)`
3432    ///     The tolerance for the f_terminator method.
3433    /// x_terminator : {"singer", "diameter", "higham", "rowan"} or None, default="singer"
3434    ///     Set the method to terminate the algorithm based on the position of simplex vertices. See [Singer]_ for details. Set to None to skip this check.
3435    /// eps_x : float, default=`MACH_EPS^(1/4)`
3436    ///     The tolerance for the x_terminator method.
3437    ///
3438    /// Particle Swarm Optimization (PSO)
3439    /// =================================
3440    /// swarm_position_initializer : {"randominlimits", "latinhypercube", "custom"}
3441    ///     The method used to initialize the swarm position. The "randominlimits" and "latinhypercube" methods require swarm_position_bounds and swarm_size to be specified, and they ignore the initial position given when constructing the swarm (this behavior may change in the future). The "custom" method requires swarm to be specified and does include the initial position.
3442    /// swarm_position_bounds : list of tuple of floats or None
3443    ///     Bounds used when randomly generating a swarm with either the "randominlimits" or "latinhypercube" swarm_position_initializer.
3444    /// swarm_size : int
3445    ///     The number of particles in the swarm when using the "randominlimits" or "latinhypercube" swarm_position_initializer.
3446    /// swarm : list of list of floats
3447    ///     A list of positions of each particle in the swarm. This argument is required when using the "custom" swarm_position_initializer.
3448    /// swarm_topology : {"global", "ring"}
3449    ///     The topology connecting particles in the swarm.
3450    /// swarm_update_method : {"sync", "synchronous", "async", "asynchronous"}
3451    ///     Synchronous updates update positions and targets in separate loops (slower but sometimes more stable) while asynchronous updates them in the same loop (faster but sometimes less stable).
3452    /// swarm_boundary_method : {"inf", "shr"}
3453    ///     The boundary method used for the swarm. "inf" sets infeasable values to +inf while "shr" shrinks the velocity vector to place the particle on the boundary where it would cross.
3454    /// use_transform : bool, default=False
3455    ///     If True, the algorithm will ignore the swarm_boundary_method and instead perform a coordinate transformation on the swarm to ensure the swarm is within bounds.
3456    /// swarm_velocity_bounds : list of tuple of floats or None, optional
3457    ///     Bounds used when randomly generating the initial velocity of each particle in the swarm. If not specified, initial velocities are set to zero.
3458    /// omega : float, default=0.8
3459    ///     The inertial weight.
3460    /// c1 : float, default = 0.1
3461    ///     The cognitive weight.
3462    /// c2 : float, default = 0.1
3463    ///     The social weight.
3464    ///
3465    /// .. rubric:: References
3466    ///
3467    /// .. [Gao] F. Gao and L. Han, “Implementing the Nelder-Mead simplex algorithm with adaptive parameters,” Comput Optim Appl, vol. 51, no. 1, pp. 259–277, May 2010, doi: 10.1007/s10589-010-9329-3.
3468    /// .. [Lagarias] J. C. Lagarias, J. A. Reeds, M. H. Wright, and P. E. Wright, “Convergence Properties of the Nelder--Mead Simplex Method in Low Dimensions,” SIAM J. Optim., vol. 9, no. 1, pp. 112–147, Jan. 1998, doi: 10.1137/s1052623496303470.
3469    /// .. [Singer] S. Singer and S. Singer, “Efficient Implementation of the Nelder–Mead Search Algorithm,” Appl Numer Analy &amp; Comput, vol. 1, no. 2, pp. 524–534, Dec. 2004, doi: 10.1002/anac.200410015.
3470    ///
3471    #[pyo3(signature = (p0, *, bounds=None, method="lbfgsb".to_string(), settings=None, observers=None, terminators=None, max_steps=None, debug=false, threads=0))]
3472    #[allow(clippy::too_many_arguments)]
3473    fn minimize<'py>(
3474        &self,
3475        py: Python<'py>,
3476        p0: Vec<Float>,
3477        bounds: Option<Vec<(Option<Float>, Option<Float>)>>,
3478        method: String,
3479        settings: Option<Bound<'py, PyDict>>,
3480        observers: Option<Bound<'_, PyAny>>,
3481        terminators: Option<Bound<'_, PyAny>>,
3482        max_steps: Option<usize>,
3483        debug: bool,
3484        threads: usize,
3485    ) -> PyResult<PyMinimizationSummary> {
3486        let full_settings = PyDict::new(py);
3487        if let Some(bounds) = bounds {
3488            full_settings.set_item("bounds", bounds)?;
3489        }
3490        full_settings.set_item("method", method)?;
3491        if let Some(observers) = observers {
3492            full_settings.set_item("observers", observers)?;
3493        }
3494        if let Some(terminators) = terminators {
3495            full_settings.set_item("terminators", terminators)?;
3496        }
3497        if let Some(max_steps) = max_steps {
3498            full_settings.set_item("max_steps", max_steps)?;
3499        }
3500        full_settings.set_item("debug", debug)?;
3501        full_settings.set_item("threads", threads)?;
3502        if let Some(settings) = settings {
3503            full_settings.set_item("settings", settings)?;
3504        }
3505        let result = self
3506            .0
3507            .minimize(MinimizationSettings::from_pyargs(&p0, &full_settings)?)?;
3508        Ok(PyMinimizationSummary(result))
3509    }
3510    /// Run an MCMC algorithm on the free parameters of the LikelihoodTerm's model
3511    ///
3512    /// This method can be used to sample the underlying likelihood term given an initial
3513    /// position for each walker `p0`.
3514    ///
3515    /// Parameters
3516    /// ----------
3517    /// p0 : array_like
3518    ///     The initial positions of each walker with dimension (n_walkers, n_parameters)
3519    /// bounds : list of tuple of float or None, optional
3520    ///     Optional bounds on each parameter (use None or an infinity for no bound)
3521    /// method : {'aies', 'ess'}
3522    ///     The MCMC algorithm to use
3523    /// settings : dict, optional
3524    ///     Settings for the MCMC algorithm (see notes)
3525    /// observers : MCMCObserver or list of MCMCObserver, optional
3526    ///     User-defined observers which are called at each step
3527    /// terminators : MCMCTerminator or list of MCMCTerminator, optional
3528    ///     User-defined terminators which are called at each step
3529    /// max_steps : int, optional
3530    ///     Set the maximum number of steps
3531    /// debug : bool, default=False
3532    ///     Use a debug observer to print out debugging information at each step
3533    /// threads : int, default=0
3534    ///     The number of threads to use (setting this to 0 will use all available CPUs)
3535    ///
3536    /// Returns
3537    /// -------
3538    /// MCMCSummary
3539    ///     The status of the MCMC algorithm at termination
3540    ///
3541    /// Raises
3542    /// ------
3543    /// Exception
3544    ///     If there was an error building the thread pool
3545    ///
3546    /// Notes
3547    /// -----
3548    /// The `settings` dict is passed to the MCMC algorithm as keyword arguments. Each
3549    /// algorithm has different settings.
3550    ///
3551    /// AIES (Affine-Invariant Ensemble Sampler) [Goodman]_
3552    /// =============================================
3553    /// moves : list of tuple of (str, float) or (str, dict, float), default = [('stretch', {'a': 2.0}, 1.0)]
3554    ///     The list of moves to use. The first element of the tuple is the name of the move
3555    ///     ('stretch' or 'walk') and the last is the frequency that move should be used relative
3556    ///     to the sum of all frequencies given across all moves. An optional middle dictionary
3557    ///     parameter may be provided to specify properties of moves which support it. For the AIES
3558    ///     algorithm, the stretch move may use the 'a' parameter to specify the scaling parameter
3559    ///     (2.0 by default).
3560    ///
3561    /// ESS (Ensemble Slice Sampler) [Karamanis]_
3562    /// =================================
3563    /// moves : list of tuple of (str, float) or (str, dict, float), default = [('differential', 1.0)]
3564    ///     The list of moves to use. The first element of the tuple is the name of the move
3565    ///     ('differential', 'gaussian', or 'global') and the last is the frequency that move
3566    ///     should be used relative to the sum of all frequencies given across all moves. An
3567    ///     optional middle dictionary parameter may be provided to specify properties of moves
3568    ///     which support it. For the ESS algorithm, the global move may use a 'scale' parameter
3569    ///     (1.0 by default) which rescales steps within a local cluster, a 'rescale_cov' parameter
3570    ///     (0.001 by default) which rescales the covariance matrix of clusters, and an
3571    ///     'n_components' parameter (5 by default) which represents the number of mixture
3572    ///     components to use for clustering (should be larger than the expected number of modes).
3573    /// n_adaptive : int, default=0
3574    ///     The number of adaptive moves to perform at the start of sampling
3575    /// max_steps : int, default=10000
3576    ///     The maximum number of expansions/contractions to perform at each step in the algorithm
3577    /// mu : float, default = 1.0
3578    ///     The adaptive scaling parameter (only applies if 'n_adaptive' is greater than zero)
3579    ///
3580    /// .. [Goodman] J. Goodman and J. Weare, “Ensemble samplers with affine invariance,” CAMCoS, vol. 5, no. 1, pp. 65–80, Jan. 2010, doi: 10.2140/camcos.2010.5.65.
3581    /// .. [Karamanis] M. Karamanis and F. Beutler, “Ensemble slice sampling,” Stat Comput, vol. 31, no. 5, Aug. 2021, doi: 10.1007/s11222-021-10038-2.
3582    ///
3583    #[pyo3(signature = (p0, *, bounds=None, method="aies".to_string(), settings=None, observers=None, terminators=None, max_steps=None, debug=false, threads=0))]
3584    #[allow(clippy::too_many_arguments)]
3585    fn mcmc<'py>(
3586        &self,
3587        py: Python<'py>,
3588        p0: Vec<Vec<Float>>,
3589        bounds: Option<Vec<(Option<Float>, Option<Float>)>>,
3590        method: String,
3591        settings: Option<Bound<'py, PyDict>>,
3592        observers: Option<Bound<'_, PyAny>>,
3593        terminators: Option<Bound<'_, PyAny>>,
3594        max_steps: Option<usize>,
3595        debug: bool,
3596        threads: usize,
3597    ) -> PyResult<PyMCMCSummary> {
3598        let full_settings = PyDict::new(py);
3599        if let Some(bounds) = bounds {
3600            full_settings.set_item("bounds", bounds)?;
3601        }
3602        full_settings.set_item("method", method)?;
3603        if let Some(observers) = observers {
3604            full_settings.set_item("observers", observers)?;
3605        }
3606        if let Some(terminators) = terminators {
3607            full_settings.set_item("terminators", terminators)?;
3608        }
3609        if let Some(max_steps) = max_steps {
3610            full_settings.set_item("max_steps", max_steps)?;
3611        }
3612        full_settings.set_item("debug", debug)?;
3613        full_settings.set_item("threads", threads)?;
3614        if let Some(settings) = settings {
3615            full_settings.set_item("settings", settings)?;
3616        }
3617        let result = self.0.mcmc(MCMCSettings::from_pyargs(
3618            &p0.into_iter().map(|p| DVector::from_vec(p)).collect(),
3619            &full_settings,
3620        )?)?;
3621        Ok(PyMCMCSummary(result))
3622    }
3623}
3624
3625/// A [`LikelihoodTerm`] which represents a single scaling parameter.
3626#[derive(Clone)]
3627pub struct LikelihoodScalar(String);
3628
3629impl LikelihoodScalar {
3630    /// Create a new [`LikelihoodScalar`] with a parameter with the given name.
3631    pub fn new<T: AsRef<str>>(name: T) -> Box<Self> {
3632        Self(name.as_ref().into()).into()
3633    }
3634}
3635
3636impl LikelihoodTerm for LikelihoodScalar {
3637    fn evaluate(&self, parameters: &[Float]) -> Float {
3638        parameters[0]
3639    }
3640
3641    fn evaluate_gradient(&self, _parameters: &[Float]) -> DVector<Float> {
3642        DVector::from_vec(vec![1.0])
3643    }
3644
3645    fn parameters(&self) -> Vec<String> {
3646        vec![self.0.clone()]
3647    }
3648}
3649
3650/// A parameterized scalar term which can be added to a LikelihoodManager
3651///
3652/// Parameters
3653/// ----------
3654/// name : str
3655///     The name of the new scalar parameter
3656///
3657/// Returns
3658/// -------
3659/// LikelihoodTerm
3660///
3661#[cfg(feature = "python")]
3662#[pyfunction(name = "LikelihoodScalar")]
3663pub fn py_likelihood_scalar(name: String) -> PyLikelihoodTerm {
3664    PyLikelihoodTerm(LikelihoodScalar::new(name))
3665}