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(&_vals),
763 self.data_evaluator
764 .expression
765 .evaluate_gradient(&_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(&_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(&_vals),
866 self.data_evaluator
867 .expression
868 .evaluate_gradient(&_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(&_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(&_vals),
1243 self.nll
1244 .data_evaluator
1245 .expression
1246 .evaluate_gradient(&_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(&_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(&_vals),
1366 self.nll
1367 .data_evaluator
1368 .expression
1369 .evaluate_gradient(&_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(&_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(), ¶meters)))
1686 }
1687 #[cfg(not(feature = "rayon"))]
1688 {
1689 Ok(LikelihoodTerm::evaluate(self.0.as_ref(), ¶meters))
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(¶meters))
1728 .as_slice(),
1729 ))
1730 }
1731 #[cfg(not(feature = "rayon"))]
1732 {
1733 Ok(PyArray1::from_slice(
1734 py,
1735 self.0.evaluate_gradient(¶meters).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(¶meters, 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(¶meters, 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 ¶meters,
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 ¶meters,
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 & 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 & 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(¶m_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(¶m_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, ¶m_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, ¶m_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(¶meters)))
3260 }
3261 #[cfg(not(feature = "rayon"))]
3262 {
3263 Ok(self.0.evaluate(¶meters))
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(¶meters))
3303 .as_slice(),
3304 ))
3305 }
3306 #[cfg(not(feature = "rayon"))]
3307 {
3308 Ok(PyArray1::from_slice(
3309 py,
3310 self.0.evaluate_gradient(¶meters).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 & 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}