Skip to main content

laddu_python/
amplitudes.rs

1use crate::data::PyDataset;
2use laddu_core::{
3    amplitudes::{constant, parameter, Evaluator, Expression, ParameterLike, TestAmplitude},
4    f64, LadduError, LadduResult, ReadWrite, ThreadPoolManager,
5};
6use num::complex::Complex64;
7use numpy::{PyArray1, PyArray2};
8use pyo3::{
9    exceptions::PyTypeError,
10    prelude::*,
11    types::{PyBytes, PyList},
12};
13use std::collections::HashMap;
14
15fn install_with_threads<R: Send>(
16    threads: Option<usize>,
17    op: impl FnOnce() -> R + Send,
18) -> LadduResult<R> {
19    ThreadPoolManager::shared().install(threads, op)
20}
21
22/// A mathematical expression formed from amplitudes.
23///
24#[pyclass(name = "Expression", module = "laddu", from_py_object)]
25#[derive(Clone)]
26pub struct PyExpression(pub Expression);
27
28/// A convenience method to sum sequences of Expressions
29///
30#[pyfunction(name = "expr_sum")]
31pub fn py_expr_sum(terms: Vec<Bound<'_, PyAny>>) -> PyResult<PyExpression> {
32    if terms.is_empty() {
33        return Ok(PyExpression(Expression::zero()));
34    }
35    if terms.len() == 1 {
36        let term = &terms[0];
37        if let Ok(expression) = term.extract::<PyExpression>() {
38            return Ok(expression);
39        }
40        return Err(PyTypeError::new_err("Item is not a PyExpression"));
41    }
42    let mut iter = terms.iter();
43    let Some(first_term) = iter.next() else {
44        return Ok(PyExpression(Expression::zero()));
45    };
46    let PyExpression(mut summation) = first_term
47        .extract::<PyExpression>()
48        .map_err(|_| PyTypeError::new_err("Elements must be PyExpression"))?;
49    for term in iter {
50        let PyExpression(expr) = term
51            .extract::<PyExpression>()
52            .map_err(|_| PyTypeError::new_err("Elements must be PyExpression"))?;
53        summation = summation + expr;
54    }
55    Ok(PyExpression(summation))
56}
57
58/// A convenience method to multiply sequences of Expressions
59///
60#[pyfunction(name = "expr_product")]
61pub fn py_expr_product(terms: Vec<Bound<'_, PyAny>>) -> PyResult<PyExpression> {
62    if terms.is_empty() {
63        return Ok(PyExpression(Expression::one()));
64    }
65    if terms.len() == 1 {
66        let term = &terms[0];
67        if let Ok(expression) = term.extract::<PyExpression>() {
68            return Ok(expression);
69        }
70        return Err(PyTypeError::new_err("Item is not a PyExpression"));
71    }
72    let mut iter = terms.iter();
73    let Some(first_term) = iter.next() else {
74        return Ok(PyExpression(Expression::one()));
75    };
76    let PyExpression(mut product) = first_term
77        .extract::<PyExpression>()
78        .map_err(|_| PyTypeError::new_err("Elements must be PyExpression"))?;
79    for term in iter {
80        let PyExpression(expr) = term
81            .extract::<PyExpression>()
82            .map_err(|_| PyTypeError::new_err("Elements must be PyExpression"))?;
83        product = product * expr;
84    }
85    Ok(PyExpression(product))
86}
87
88/// A convenience class representing a zero-valued Expression
89///
90#[pyfunction(name = "Zero")]
91pub fn py_expr_zero() -> PyExpression {
92    PyExpression(Expression::zero())
93}
94
95/// A convenience class representing a unit-valued Expression
96///
97#[pyfunction(name = "One")]
98pub fn py_expr_one() -> PyExpression {
99    PyExpression(Expression::one())
100}
101
102#[pymethods]
103impl PyExpression {
104    /// The free parameters used by the Expression
105    ///
106    /// Returns
107    /// -------
108    /// parameters : list of str
109    ///     The list of parameter names
110    #[getter]
111    fn parameters(&self) -> Vec<String> {
112        self.0.parameters()
113    }
114    /// The free parameters used by the Expression
115    #[getter]
116    fn free_parameters(&self) -> Vec<String> {
117        self.0.free_parameters()
118    }
119    /// The fixed parameters used by the Expression
120    #[getter]
121    fn fixed_parameters(&self) -> Vec<String> {
122        self.0.fixed_parameters()
123    }
124    /// Number of free parameters
125    #[getter]
126    fn n_free(&self) -> usize {
127        self.0.n_free()
128    }
129    /// Number of fixed parameters
130    #[getter]
131    fn n_fixed(&self) -> usize {
132        self.0.n_fixed()
133    }
134    /// Total number of parameters
135    #[getter]
136    fn n_parameters(&self) -> usize {
137        self.0.n_parameters()
138    }
139    /// Load an Expression by precalculating each term over the given Dataset
140    ///
141    /// Parameters
142    /// ----------
143    /// dataset : Dataset
144    ///     The Dataset to use in precalculation
145    ///
146    /// Returns
147    /// -------
148    /// Evaluator
149    ///     An object that can be used to evaluate the `expression` over each event in the
150    ///     `dataset`
151    fn load(&self, dataset: &PyDataset) -> PyResult<PyEvaluator> {
152        Ok(PyEvaluator(self.0.load(&dataset.0)?))
153    }
154    /// The real part of a complex Expression
155    fn real(&self) -> PyExpression {
156        PyExpression(self.0.real())
157    }
158    /// The imaginary part of a complex Expression
159    fn imag(&self) -> PyExpression {
160        PyExpression(self.0.imag())
161    }
162    /// The complex conjugate of a complex Expression
163    fn conj(&self) -> PyExpression {
164        PyExpression(self.0.conj())
165    }
166    /// The norm-squared of a complex Expression
167    fn norm_sqr(&self) -> PyExpression {
168        PyExpression(self.0.norm_sqr())
169    }
170    /// Return a new Expression with the given parameter fixed
171    fn fix(&self, name: &str, value: f64) -> PyResult<PyExpression> {
172        Ok(PyExpression(self.0.fix(name, value)?))
173    }
174    /// Return a new Expression with the given parameter freed
175    fn free(&self, name: &str) -> PyResult<PyExpression> {
176        Ok(PyExpression(self.0.free(name)?))
177    }
178    /// Return a new Expression with a single parameter renamed
179    fn rename_parameter(&self, old: &str, new: &str) -> PyResult<PyExpression> {
180        Ok(PyExpression(self.0.rename_parameter(old, new)?))
181    }
182    /// Return a new Expression with several parameters renamed
183    fn rename_parameters(&self, mapping: HashMap<String, String>) -> PyResult<PyExpression> {
184        Ok(PyExpression(self.0.rename_parameters(&mapping)?))
185    }
186    fn __add__(&self, other: &Bound<'_, PyAny>) -> PyResult<PyExpression> {
187        if let Ok(other_expr) = other.extract::<PyExpression>() {
188            Ok(PyExpression(self.0.clone() + other_expr.0))
189        } else if let Ok(other_int) = other.extract::<usize>() {
190            if other_int == 0 {
191                Ok(PyExpression(self.0.clone()))
192            } else {
193                Err(PyTypeError::new_err(
194                    "Addition with an integer for this type is only defined for 0",
195                ))
196            }
197        } else {
198            Err(PyTypeError::new_err("Unsupported operand type for +"))
199        }
200    }
201    fn __radd__(&self, other: &Bound<'_, PyAny>) -> PyResult<PyExpression> {
202        if let Ok(other_expr) = other.extract::<PyExpression>() {
203            Ok(PyExpression(other_expr.0 + self.0.clone()))
204        } else if let Ok(other_int) = other.extract::<usize>() {
205            if other_int == 0 {
206                Ok(PyExpression(self.0.clone()))
207            } else {
208                Err(PyTypeError::new_err(
209                    "Addition with an integer for this type is only defined for 0",
210                ))
211            }
212        } else {
213            Err(PyTypeError::new_err("Unsupported operand type for +"))
214        }
215    }
216    fn __sub__(&self, other: &Bound<'_, PyAny>) -> PyResult<PyExpression> {
217        if let Ok(other_expr) = other.extract::<PyExpression>() {
218            Ok(PyExpression(self.0.clone() - other_expr.0))
219        } else {
220            Err(PyTypeError::new_err("Unsupported operand type for -"))
221        }
222    }
223    fn __rsub__(&self, other: &Bound<'_, PyAny>) -> PyResult<PyExpression> {
224        if let Ok(other_expr) = other.extract::<PyExpression>() {
225            Ok(PyExpression(other_expr.0 - self.0.clone()))
226        } else {
227            Err(PyTypeError::new_err("Unsupported operand type for -"))
228        }
229    }
230    fn __mul__(&self, other: &Bound<'_, PyAny>) -> PyResult<PyExpression> {
231        if let Ok(other_expr) = other.extract::<PyExpression>() {
232            Ok(PyExpression(self.0.clone() * other_expr.0))
233        } else {
234            Err(PyTypeError::new_err("Unsupported operand type for *"))
235        }
236    }
237    fn __rmul__(&self, other: &Bound<'_, PyAny>) -> PyResult<PyExpression> {
238        if let Ok(other_expr) = other.extract::<PyExpression>() {
239            Ok(PyExpression(other_expr.0 * self.0.clone()))
240        } else {
241            Err(PyTypeError::new_err("Unsupported operand type for *"))
242        }
243    }
244    fn __truediv__(&self, other: &Bound<'_, PyAny>) -> PyResult<PyExpression> {
245        if let Ok(other_expr) = other.extract::<PyExpression>() {
246            Ok(PyExpression(self.0.clone() / other_expr.0))
247        } else {
248            Err(PyTypeError::new_err("Unsupported operand type for /"))
249        }
250    }
251    fn __rtruediv__(&self, other: &Bound<'_, PyAny>) -> PyResult<PyExpression> {
252        if let Ok(other_expr) = other.extract::<PyExpression>() {
253            Ok(PyExpression(other_expr.0 / self.0.clone()))
254        } else {
255            Err(PyTypeError::new_err("Unsupported operand type for /"))
256        }
257    }
258    fn __neg__(&self) -> PyExpression {
259        PyExpression(-self.0.clone())
260    }
261    fn __str__(&self) -> String {
262        format!("{}", self.0)
263    }
264    fn __repr__(&self) -> String {
265        format!("{:?}", self.0)
266    }
267
268    #[new]
269    fn new() -> Self {
270        Self(Expression::create_null())
271    }
272    fn __getstate__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
273        Ok(PyBytes::new(
274            py,
275            serde_pickle::to_vec(&self.0, serde_pickle::SerOptions::new())
276                .map_err(LadduError::PickleError)?
277                .as_slice(),
278        ))
279    }
280    fn __setstate__(&mut self, state: Bound<'_, PyBytes>) -> PyResult<()> {
281        *self = Self(
282            serde_pickle::from_slice(state.as_bytes(), serde_pickle::DeOptions::new())
283                .map_err(LadduError::PickleError)?,
284        );
285        Ok(())
286    }
287}
288
289/// A class which can be used to evaluate a stored Expression
290///
291/// See Also
292/// --------
293/// laddu.Expression.load
294///
295#[pyclass(name = "Evaluator", module = "laddu", from_py_object)]
296#[derive(Clone)]
297pub struct PyEvaluator(pub Evaluator);
298
299#[pymethods]
300impl PyEvaluator {
301    /// The free parameters used by the Evaluator
302    ///
303    /// Returns
304    /// -------
305    /// parameters : list of str
306    ///     The list of parameter names
307    ///
308    #[getter]
309    fn parameters(&self) -> Vec<String> {
310        self.0.parameters()
311    }
312    /// The free parameters used by the Evaluator
313    #[getter]
314    fn free_parameters(&self) -> Vec<String> {
315        self.0.free_parameters()
316    }
317    /// The fixed parameters used by the Evaluator
318    #[getter]
319    fn fixed_parameters(&self) -> Vec<String> {
320        self.0.fixed_parameters()
321    }
322    /// Number of free parameters
323    #[getter]
324    fn n_free(&self) -> usize {
325        self.0.n_free()
326    }
327    /// Number of fixed parameters
328    #[getter]
329    fn n_fixed(&self) -> usize {
330        self.0.n_fixed()
331    }
332    /// Total number of parameters
333    #[getter]
334    fn n_parameters(&self) -> usize {
335        self.0.n_parameters()
336    }
337    /// Return a new Evaluator with the given parameter fixed
338    fn fix(&self, name: &str, value: f64) -> PyResult<PyEvaluator> {
339        Ok(PyEvaluator(self.0.fix(name, value)?))
340    }
341    /// Return a new Evaluator with the given parameter freed
342    fn free(&self, name: &str) -> PyResult<PyEvaluator> {
343        Ok(PyEvaluator(self.0.free(name)?))
344    }
345    /// Return a new Evaluator with a single parameter renamed
346    fn rename_parameter(&self, old: &str, new: &str) -> PyResult<PyEvaluator> {
347        Ok(PyEvaluator(self.0.rename_parameter(old, new)?))
348    }
349    /// Return a new Evaluator with several parameters renamed
350    fn rename_parameters(&self, mapping: HashMap<String, String>) -> PyResult<PyEvaluator> {
351        Ok(PyEvaluator(self.0.rename_parameters(&mapping)?))
352    }
353    /// Activates Amplitudes in the Expression by name
354    ///
355    /// Parameters
356    /// ----------
357    /// arg : str or list of str
358    ///     Names of Amplitudes to be activated
359    ///
360    /// Raises
361    /// ------
362    /// TypeError
363    ///     If `arg` is not a str or list of str
364    /// ValueError
365    ///     If `arg` or any items of `arg` are not registered Amplitudes
366    /// strict : bool, default=True
367    ///     When ``True``, raise an error if any amplitude is missing. When ``False``,
368    ///     silently skip missing amplitudes.
369    #[pyo3(signature = (arg, *, strict=true))]
370    fn activate(&self, arg: &Bound<'_, PyAny>, strict: bool) -> PyResult<()> {
371        if let Ok(string_arg) = arg.extract::<String>() {
372            if strict {
373                self.0.activate_strict(&string_arg)?;
374            } else {
375                self.0.activate(&string_arg);
376            }
377        } else if let Ok(list_arg) = arg.cast::<PyList>() {
378            let vec: Vec<String> = list_arg.extract()?;
379            if strict {
380                self.0.activate_many_strict(&vec)?;
381            } else {
382                self.0.activate_many(&vec);
383            }
384        } else {
385            return Err(PyTypeError::new_err(
386                "Argument must be either a string or a list of strings",
387            ));
388        }
389        Ok(())
390    }
391    /// Activates all Amplitudes in the Expression
392    ///
393    fn activate_all(&self) {
394        self.0.activate_all();
395    }
396    /// Deactivates Amplitudes in the Expression by name
397    ///
398    /// Deactivated Amplitudes act as zeros in the Expression
399    ///
400    /// Parameters
401    /// ----------
402    /// arg : str or list of str
403    ///     Names of Amplitudes to be deactivated
404    ///
405    /// Raises
406    /// ------
407    /// TypeError
408    ///     If `arg` is not a str or list of str
409    /// ValueError
410    ///     If `arg` or any items of `arg` are not registered Amplitudes
411    /// strict : bool, default=True
412    ///     When ``True``, raise an error if any amplitude is missing. When ``False``,
413    ///     silently skip missing amplitudes.
414    #[pyo3(signature = (arg, *, strict=true))]
415    fn deactivate(&self, arg: &Bound<'_, PyAny>, strict: bool) -> PyResult<()> {
416        if let Ok(string_arg) = arg.extract::<String>() {
417            if strict {
418                self.0.deactivate_strict(&string_arg)?;
419            } else {
420                self.0.deactivate(&string_arg);
421            }
422        } else if let Ok(list_arg) = arg.cast::<PyList>() {
423            let vec: Vec<String> = list_arg.extract()?;
424            if strict {
425                self.0.deactivate_many_strict(&vec)?;
426            } else {
427                self.0.deactivate_many(&vec);
428            }
429        } else {
430            return Err(PyTypeError::new_err(
431                "Argument must be either a string or a list of strings",
432            ));
433        }
434        Ok(())
435    }
436    /// Deactivates all Amplitudes in the Expression
437    ///
438    fn deactivate_all(&self) {
439        self.0.deactivate_all();
440    }
441    /// Isolates Amplitudes in the Expression by name
442    ///
443    /// Activates the Amplitudes given in `arg` and deactivates the rest
444    ///
445    /// Parameters
446    /// ----------
447    /// arg : str or list of str
448    ///     Names of Amplitudes to be isolated
449    ///
450    /// Raises
451    /// ------
452    /// TypeError
453    ///     If `arg` is not a str or list of str
454    /// ValueError
455    ///     If `arg` or any items of `arg` are not registered Amplitudes
456    /// strict : bool, default=True
457    ///     When ``True``, raise an error if any amplitude is missing. When ``False``,
458    ///     silently skip missing amplitudes.
459    #[pyo3(signature = (arg, *, strict=true))]
460    fn isolate(&self, arg: &Bound<'_, PyAny>, strict: bool) -> PyResult<()> {
461        if let Ok(string_arg) = arg.extract::<String>() {
462            if strict {
463                self.0.isolate_strict(&string_arg)?;
464            } else {
465                self.0.isolate(&string_arg);
466            }
467        } else if let Ok(list_arg) = arg.cast::<PyList>() {
468            let vec: Vec<String> = list_arg.extract()?;
469            if strict {
470                self.0.isolate_many_strict(&vec)?;
471            } else {
472                self.0.isolate_many(&vec);
473            }
474        } else {
475            return Err(PyTypeError::new_err(
476                "Argument must be either a string or a list of strings",
477            ));
478        }
479        Ok(())
480    }
481
482    /// Return the current active-amplitude mask.
483    #[getter]
484    fn active_mask(&self) -> Vec<bool> {
485        self.0.active_mask()
486    }
487
488    /// Apply an active-amplitude mask.
489    fn set_active_mask(&self, mask: Vec<bool>) -> PyResult<()> {
490        self.0.set_active_mask(&mask)?;
491        Ok(())
492    }
493
494    /// Evaluate the stored Expression over the stored Dataset
495    ///
496    /// Parameters
497    /// ----------
498    /// parameters : list of float
499    ///     The values to use for the free parameters
500    /// threads : int, optional
501    ///     The number of threads to use (setting this to ``None`` or ``0`` uses the current
502    ///     global or context-managed default; any positive value overrides that default for
503    ///     this call only)
504    ///
505    /// Returns
506    /// -------
507    /// result : array_like
508    ///     A ``numpy`` array of complex values for each Event in the Dataset
509    ///
510    /// Raises
511    /// ------
512    /// Exception
513    ///     If there was an error building the thread pool
514    ///
515    #[pyo3(signature = (parameters, *, threads=None))]
516    fn evaluate<'py>(
517        &self,
518        py: Python<'py>,
519        parameters: Vec<f64>,
520        threads: Option<usize>,
521    ) -> PyResult<Bound<'py, PyArray1<Complex64>>> {
522        let values = install_with_threads(threads, || self.0.evaluate(&parameters))?;
523        Ok(PyArray1::from_slice(py, &values))
524    }
525    /// Evaluate the stored Expression over a subset of the stored Dataset
526    ///
527    /// Parameters
528    /// ----------
529    /// parameters : list of float
530    ///     The values to use for the free parameters
531    /// indices : list of int
532    ///     The indices of events to evaluate
533    /// threads : int, optional
534    ///     The number of threads to use (setting this to ``None`` or ``0`` uses the current
535    ///     global or context-managed default; any positive value overrides that default for
536    ///     this call only)
537    ///
538    /// Returns
539    /// -------
540    /// result : array_like
541    ///     A ``numpy`` array of complex values for each indexed Event in the Dataset
542    ///
543    /// Raises
544    /// ------
545    /// Exception
546    ///     If there was an error building the thread pool
547    ///
548    #[pyo3(signature = (parameters, indices, *, threads=None))]
549    fn evaluate_batch<'py>(
550        &self,
551        py: Python<'py>,
552        parameters: Vec<f64>,
553        indices: Vec<usize>,
554        threads: Option<usize>,
555    ) -> PyResult<Bound<'py, PyArray1<Complex64>>> {
556        let values =
557            install_with_threads(threads, || self.0.evaluate_batch(&parameters, &indices))?;
558        Ok(PyArray1::from_slice(py, &values))
559    }
560    /// Evaluate the gradient of the stored Expression over the stored Dataset
561    ///
562    /// Parameters
563    /// ----------
564    /// parameters : list of float
565    ///     The values to use for the free parameters
566    /// threads : int, optional
567    ///     The number of threads to use (setting this to ``None`` or ``0`` uses the current
568    ///     global or context-managed default; any positive value overrides that default for
569    ///     this call only)
570    ///
571    /// Returns
572    /// -------
573    /// result : array_like
574    ///     A ``numpy`` 2D array of complex values for each Event in the Dataset
575    ///
576    /// Raises
577    /// ------
578    /// Exception
579    ///     If there was an error building the thread pool or problem creating the resulting
580    ///     ``numpy`` array
581    ///
582    #[pyo3(signature = (parameters, *, threads=None))]
583    fn evaluate_gradient<'py>(
584        &self,
585        py: Python<'py>,
586        parameters: Vec<f64>,
587        threads: Option<usize>,
588    ) -> PyResult<Bound<'py, PyArray2<Complex64>>> {
589        let gradients = install_with_threads(threads, || {
590            self.0
591                .evaluate_gradient(&parameters)
592                .iter()
593                .map(|grad| grad.data.as_vec().to_vec())
594                .collect::<Vec<Vec<Complex64>>>()
595        })?;
596        Ok(PyArray2::from_vec2(py, &gradients).map_err(LadduError::NumpyError)?)
597    }
598    /// Evaluate the gradient of the stored Expression over a subset of the stored Dataset
599    ///
600    /// Parameters
601    /// ----------
602    /// parameters : list of float
603    ///     The values to use for the free parameters
604    /// indices : list of int
605    ///     The indices of events to evaluate
606    /// threads : int, optional
607    ///     The number of threads to use (setting this to ``None`` or ``0`` uses the current
608    ///     global or context-managed default; any positive value overrides that default for
609    ///     this call only)
610    ///
611    /// Returns
612    /// -------
613    /// result : array_like
614    ///     A ``numpy`` 2D array of complex values for each indexed Event in the Dataset
615    ///
616    /// Raises
617    /// ------
618    /// Exception
619    ///     If there was an error building the thread pool or problem creating the resulting
620    ///     ``numpy`` array
621    ///
622    #[pyo3(signature = (parameters, indices, *, threads=None))]
623    fn evaluate_gradient_batch<'py>(
624        &self,
625        py: Python<'py>,
626        parameters: Vec<f64>,
627        indices: Vec<usize>,
628        threads: Option<usize>,
629    ) -> PyResult<Bound<'py, PyArray2<Complex64>>> {
630        let gradients = install_with_threads(threads, || {
631            self.0
632                .evaluate_gradient_batch(&parameters, &indices)
633                .iter()
634                .map(|grad| grad.data.as_vec().to_vec())
635                .collect::<Vec<Vec<Complex64>>>()
636        })?;
637        Ok(PyArray2::from_vec2(py, &gradients).map_err(LadduError::NumpyError)?)
638    }
639}
640
641/// A class, typically used to allow Amplitudes to take either free parameters or constants as
642/// inputs
643///
644/// See Also
645/// --------
646/// laddu.parameter
647/// laddu.constant
648///
649#[pyclass(name = "ParameterLike", module = "laddu", from_py_object)]
650#[derive(Clone)]
651pub struct PyParameterLike(pub ParameterLike);
652
653/// A free parameter which floats during an optimization
654///
655/// Parameters
656/// ----------
657/// name : str
658///     The name of the free parameter
659///
660/// Returns
661/// -------
662/// laddu.ParameterLike
663///     An object that can be used as the input for many Amplitude constructors
664///
665/// Notes
666/// -----
667/// Two free parameters with the same name are shared in a fit
668///
669#[pyfunction(name = "parameter")]
670pub fn py_parameter(name: &str) -> PyParameterLike {
671    PyParameterLike(parameter(name))
672}
673
674/// A term which stays constant during an optimization
675///
676/// Parameters
677/// ----------
678/// name : str
679///     The name of the parameter
680/// value : float
681///     The numerical value of the constant
682///
683/// Returns
684/// -------
685/// laddu.ParameterLike
686///     An object that can be used as the input for many Amplitude constructors
687///
688#[pyfunction(name = "constant")]
689pub fn py_constant(name: &str, value: f64) -> PyParameterLike {
690    PyParameterLike(constant(name, value))
691}
692
693/// An amplitude used only for internal testing which evaluates `(p0 + i * p1) * event.p4s\[0\].e`.
694#[pyfunction(name = "TestAmplitude")]
695pub fn py_test_amplitude(
696    name: &str,
697    re: PyParameterLike,
698    im: PyParameterLike,
699) -> PyResult<PyExpression> {
700    Ok(PyExpression(TestAmplitude::new(name, re.0, im.0)?))
701}