Skip to main content

gammalooprs/processes/
mod.rs

1use std::{fs, path::Path};
2
3// use bincode::{Decode, Encode};
4use bincode_trait_derive::{Decode, Encode};
5use color_eyre::Result;
6use eyre::{Context, eyre};
7use rayon::ThreadPool;
8use schemars::JsonSchema;
9use symbolica::evaluate::OptimizationSettings;
10use tracing::debug;
11
12use crate::{
13    GammaLoopContext, GammaLoopContextContainer,
14    settings::{GlobalSettings, runtime::LockedRuntimeSettings},
15    utils::serde_utils::{IsDefault, is_false, is_true, is_usize, show_defaults_helper},
16    uv::export::UVForestExportSettings,
17};
18use serde::{Deserialize, Serialize};
19
20use crate::model::Model;
21
22mod generation_report;
23pub use generation_report::{
24    EvaluatorBuildTimings, GeneratedGraphKey, GeneratedGraphReport, GraphGenerationStats,
25    NamedGraphGenerationReport, merge_generated_graph_reports,
26};
27mod generation_progress;
28pub use generation_progress::{
29    GenerationProcessKind, GenerationProgressMode, GenerationProgressModeGuard,
30    GenerationProgressObserver, GenerationProgressObserverGuard, GenerationProgressPhase,
31    begin_phase, cut_finished, detailed_progress_enabled, detailed_progress_message,
32    enter_detailed_progress_span, enter_progress_context,
33};
34mod selection;
35pub use selection::{
36    CycleSignature, GraphGroupSelectionMode, GraphGroupSelectionPlan, GraphGroupSelectionReport,
37    GraphGroupSelectionSpec, GraphSelectionSignatureInventory, ParticleSignature,
38    RaisedCutSignatureInventory, RaisedPropagatorScope, RaisedPropagatorSignature,
39    SelectionPolarity, VertexSignature,
40};
41pub(crate) use selection::{GraphCutSelectionSubject, GraphSelectionSubject};
42
43#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
44#[derive(
45    Debug, Clone, Copy, Default, Serialize, Deserialize, Encode, Decode, PartialEq, Eq, JsonSchema,
46)]
47#[serde(rename_all = "snake_case")]
48pub enum TensorNetworkContractionOrder {
49    #[default]
50    SparseAtomAware,
51    AtomAware,
52    ResultRankOnly,
53    EntryAware,
54}
55
56#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
57#[derive(Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
58pub enum ExecutionMode {
59    Sequential,
60    Parallel,
61    SequentialRef,
62    SequentialExtract,
63}
64
65#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
66#[derive(Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
67pub enum ContractionMode {
68    SmallestDegree,
69    MinResultRank,
70}
71#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
72#[derive(Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
73pub struct EvaluatorSettings {
74    /// Perform symbolic tensor and scalar algebra before lowering to an evaluator.
75    #[serde(default, skip_serializing_if = "is_false")]
76    pub do_algebra: bool,
77    /// Re-optimize orientation expressions iteratively while building the evaluator.
78    #[serde(
79        default = "evaluator_default_iterative_orientation_optimization",
80        skip_serializing_if = "is_true"
81    )]
82    pub iterative_orientation_optimization: bool,
83    /// Build one evaluator for the sum of all contributions instead of separate evaluators.
84    #[serde(default, skip_serializing_if = "is_false")]
85    pub summed: bool,
86    /// Represent a summed evaluator through a shared Symbolica function map.
87    #[serde(default, skip_serializing_if = "is_false")]
88    pub summed_function_map: bool,
89    /// Compile the lowered evaluator using the configured generation compilation backend.
90    #[serde(default, skip_serializing_if = "is_false")]
91    pub compile: bool,
92    /// Retain the intermediate Symbolica atom alongside the executable evaluator.
93    #[serde(default, skip_serializing_if = "is_false")]
94    pub store_atom: bool,
95    /// Apply function-map replacements before numerical lowering.
96    #[serde(default, skip_serializing_if = "is_false")]
97    pub do_fn_map_replacements: bool,
98    /// Translate compatible symbolic expressions directly into Spenso networks.
99    #[serde(
100        default = "evaluator_default_direct_translation",
101        skip_serializing_if = "is_true"
102    )]
103    pub direct_translation: bool,
104    /// Number of Horner-scheme optimization passes applied to scalar expressions.
105    #[serde(
106        default = "evaluator_default_horner_iterations",
107        skip_serializing_if = "is_usize::<1>"
108    )]
109    pub horner_iterations: usize,
110    /// Worker threads used while constructing one evaluator.
111    #[serde(
112        default = "evaluator_default_n_cores",
113        skip_serializing_if = "is_usize::<1>"
114    )]
115    pub n_cores: usize,
116    /// Optional number of common-pair-elimination optimization passes.
117    #[serde(default, skip_serializing_if = "IsDefault::is_default")]
118    pub cpe_iterations: Option<usize>,
119    /// Optimization stage at which evaluator construction aborts for diagnostic isolation.
120    #[serde(default, skip_serializing_if = "IsDefault::is_default")]
121    pub abort_level: usize,
122
123    /// Maximum variable count for attempting a Horner-scheme optimization.
124    #[serde(
125        default = "evaluator_default_max_horner_scheme_variables",
126        skip_serializing_if = "is_usize::<500>"
127    )]
128    pub max_horner_scheme_variables: usize,
129
130    /// Maximum cached candidate pairs during common-pair elimination.
131    #[serde(
132        default = "evaluator_default_max_common_pair_cache_entries",
133        skip_serializing_if = "is_usize::<1000000>"
134    )]
135    pub max_common_pair_cache_entries: usize,
136
137    /// Maximum expression-tree distance considered during common-pair elimination.
138    #[serde(
139        default = "evaluator_default_max_common_pair_distance",
140        skip_serializing_if = "is_usize::<1000>"
141    )]
142    pub max_common_pair_distance: usize,
143    /// Heuristic used to order tensor-network contractions.
144    #[serde(default, skip_serializing_if = "IsDefault::is_default")]
145    pub tensor_network_contraction_order: TensorNetworkContractionOrder,
146    /// Emit detailed evaluator-construction diagnostics.
147    #[serde(default, skip_serializing_if = "is_false")]
148    pub verbose: bool,
149
150    /// Spenso execution and contraction modes used by the lowered tensor network.
151    #[serde(
152        default = "evaluator_default_spenso_execution_mode",
153        skip_serializing_if = "is_default_spenso_execution_mode"
154    )]
155    pub spenso_execution_mode: (ExecutionMode, ContractionMode),
156}
157
158const fn evaluator_default_iterative_orientation_optimization() -> bool {
159    true
160}
161
162const fn evaluator_default_direct_translation() -> bool {
163    true
164}
165
166const fn evaluator_default_horner_iterations() -> usize {
167    1
168}
169
170const fn evaluator_default_n_cores() -> usize {
171    1
172}
173
174const fn evaluator_default_max_horner_scheme_variables() -> usize {
175    500
176}
177
178const fn evaluator_default_max_common_pair_cache_entries() -> usize {
179    1_000_000
180}
181
182const fn evaluator_default_max_common_pair_distance() -> usize {
183    1000
184}
185
186const fn evaluator_default_spenso_execution_mode() -> (ExecutionMode, ContractionMode) {
187    (ExecutionMode::Sequential, ContractionMode::MinResultRank)
188}
189
190fn is_default_spenso_execution_mode(mode: &(ExecutionMode, ContractionMode)) -> bool {
191    show_defaults_helper(mode == &evaluator_default_spenso_execution_mode())
192}
193
194impl Default for EvaluatorSettings {
195    fn default() -> Self {
196        Self {
197            iterative_orientation_optimization:
198                evaluator_default_iterative_orientation_optimization(),
199            summed: false,
200            do_algebra: false,
201            summed_function_map: false,
202            direct_translation: evaluator_default_direct_translation(),
203            compile: false,
204            do_fn_map_replacements: false,
205            store_atom: false,
206            horner_iterations: evaluator_default_horner_iterations(),
207            n_cores: evaluator_default_n_cores(),
208            cpe_iterations: None,
209            abort_level: 0,
210            max_horner_scheme_variables: evaluator_default_max_horner_scheme_variables(),
211            max_common_pair_cache_entries: evaluator_default_max_common_pair_cache_entries(),
212            max_common_pair_distance: evaluator_default_max_common_pair_distance(),
213            tensor_network_contraction_order: TensorNetworkContractionOrder::default(),
214            verbose: false,
215            spenso_execution_mode: evaluator_default_spenso_execution_mode(),
216        }
217    }
218}
219
220impl EvaluatorSettings {
221    pub fn optimization_settings(&self) -> OptimizationSettings {
222        OptimizationSettings::new()
223            .horner_iterations(self.horner_iterations)
224            .cores(self.n_cores)
225            .cpe_iterations(self.cpe_iterations)
226            .abort_check(Some(Box::new(
227                crate::is_interrupt_requested as fn() -> bool,
228            )))
229            .abort_level(self.abort_level)
230            .max_horner_scheme_variables(self.max_horner_scheme_variables)
231            .max_common_pair_cache_entries(self.max_common_pair_cache_entries)
232            .max_common_pair_distance(self.max_common_pair_distance)
233            .verbose(self.verbose)
234            .direct_translation(self.direct_translation)
235    }
236}
237
238#[derive(Clone, Encode, Decode)]
239#[trait_decode(trait = GammaLoopContext)]
240pub struct ProcessList {
241    // pub amplitude_process: Vec<Process<Amplitude>>
242    // pub cross_section_process: Vec<Process<CrossSection>>
243    pub processes: Vec<Process>,
244}
245
246/// Controls which graph details and algebraic transformations are included in DOT exports.
247#[derive(Serialize, Deserialize, Debug, Clone)]
248#[cfg_attr(feature = "python_stubgen", pyo3_stub_gen::derive::gen_stub_pyclass)]
249#[cfg_attr(
250    feature = "python_api",
251    pyo3::pyclass(from_py_object, get_all, set_all)
252)]
253pub struct DotExportSettings {
254    // pub root_folder: PathBuf,
255    /// Write all diagrams of an integrand to one file during filesystem export.
256    pub combine_diagrams: bool,
257    /// Request UV counterterm graph output; currently retained for compatibility.
258    pub with_uv: bool,
259    /// Add the complete symbolic numerator as the graph-level `full_num` field.
260    pub output_full_numerator: bool,
261    /// Split cross-section graphs by distinct initial-state assignments.
262    pub split_xs_by_initial_states: bool,
263    /// Simplify gamma-matrix algebra in the full numerator before formatting it.
264    pub do_gamma_algebra: bool,
265    /// Simplify color algebra in the full numerator before formatting it.
266    pub do_color_algebra: bool,
267    #[serde(default, skip_serializing_if = "is_false")]
268    /// Include vertex and edge fields whose values GammaLoop generated automatically.
269    pub include_autogenerated_fields: bool,
270}
271
272#[derive(Serialize, Deserialize, Debug, Clone)]
273#[cfg_attr(
274    feature = "python_api",
275    pyo3::pyclass(from_py_object, get_all, set_all)
276)]
277pub struct StandaloneExportSettings {
278    #[serde(default)]
279    pub mode: StandaloneExportMode,
280    #[serde(default)]
281    pub format: StandaloneDataFormat,
282    #[serde(default)]
283    pub precision: StandaloneNumericTarget,
284}
285
286#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
287#[derive(
288    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode, JsonSchema,
289)]
290#[serde(rename_all = "snake_case")]
291pub enum StandaloneExportMode {
292    #[default]
293    Rust,
294    Python,
295}
296
297#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
298#[derive(
299    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode, JsonSchema,
300)]
301#[serde(rename_all = "snake_case")]
302pub enum StandaloneDataFormat {
303    #[default]
304    Binary,
305    Json,
306}
307
308#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
309#[derive(
310    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode, JsonSchema,
311)]
312#[serde(rename_all = "snake_case")]
313pub enum StandaloneNumericTarget {
314    #[default]
315    Double,
316    Quad,
317    Arb,
318}
319
320#[cfg(feature = "python_api")]
321#[cfg_attr(feature = "python_stubgen", pyo3_stub_gen::derive::gen_stub_pymethods)]
322#[cfg_attr(feature = "python_api", pyo3::pymethods)]
323impl DotExportSettings {
324    /// Construct DOT export settings with GammaLoop's defaults.
325    #[new]
326    fn new() -> Self {
327        DotExportSettings::default()
328    }
329}
330
331impl Default for DotExportSettings {
332    fn default() -> Self {
333        Self {
334            // root_folder: PathBuf::from("."),
335            split_xs_by_initial_states: false,
336            output_full_numerator: false,
337            do_gamma_algebra: false,
338            do_color_algebra: true,
339            combine_diagrams: false,
340            with_uv: false,
341            include_autogenerated_fields: false,
342        }
343    }
344}
345
346impl Default for StandaloneExportSettings {
347    fn default() -> Self {
348        Self {
349            format: StandaloneDataFormat::Binary,
350            mode: StandaloneExportMode::Rust,
351            precision: StandaloneNumericTarget::Double,
352        }
353    }
354}
355
356impl Default for ProcessList {
357    fn default() -> Self {
358        Self::new()
359    }
360}
361
362// Python Api
363impl ProcessList {
364    /// Generates a new empty process list
365    pub fn new() -> Self {
366        ProcessList { processes: vec![] }
367    }
368
369    pub fn warm_up_all_processes(&mut self, model: &Model) -> Result<()> {
370        for process in &mut self.processes.iter_mut() {
371            process.warm_up(model)?;
372        }
373
374        Ok(())
375    }
376
377    pub fn load(path: impl AsRef<Path>, context: GammaLoopContextContainer) -> Result<Self> {
378        let mut process_list = Self::new();
379
380        let path = path.as_ref().join("processes");
381        let amplitudes_path = path.join("amplitudes");
382        if amplitudes_path.exists() {
383            debug!("Looking for amplitudes in {}", amplitudes_path.display());
384
385            for entry in fs::read_dir(amplitudes_path).context("Error reading dir")? {
386                let Ok(entry) = entry else {
387                    debug!("Skipping invalid entry");
388
389                    continue;
390                };
391                let path = entry.path();
392                process_list.processes.push(
393                    Process::load_amplitude(path, context).context("Error loading amplitude")?,
394                );
395            }
396        }
397
398        let cross_sections_path = path.join("cross_sections");
399        if cross_sections_path.exists() {
400            debug!(
401                "Looking for cross sections in {}",
402                cross_sections_path.display()
403            );
404
405            for entry in fs::read_dir(cross_sections_path)? {
406                let entry = entry?;
407                let path = entry.path();
408                process_list
409                    .processes
410                    .push(Process::load_cross_section(path, context)?);
411            }
412        }
413        process_list
414            .processes
415            .sort_by_key(|p| p.definition.process_id);
416
417        Ok(process_list)
418    }
419
420    pub fn save(&mut self, folder: impl AsRef<Path>, override_existing: bool) -> Result<()> {
421        let path = folder.as_ref().join("processes");
422
423        let r = fs::create_dir_all(&path);
424        if !override_existing {
425            r?;
426        }
427
428        for p in self.processes.iter_mut() {
429            p.save(&path, override_existing)?;
430        }
431
432        Ok(())
433    }
434
435    pub fn compile(
436        &mut self,
437        folder: impl AsRef<Path>,
438        override_existing: bool,
439        process_id: Option<usize>,
440        integrand_name: Option<String>,
441        thread_pool: &ThreadPool,
442    ) -> Result<Vec<GeneratedGraphReport>> {
443        let path = folder.as_ref().join("processes");
444
445        let r = fs::create_dir_all(&path);
446        if !override_existing {
447            r?;
448        }
449
450        let mut reports = Vec::new();
451        for p in self.processes.iter_mut() {
452            if let Some(id) = process_id
453                && p.definition.process_id != id
454            {
455                continue;
456            }
457            reports.extend(p.compile(
458                &path,
459                override_existing,
460                integrand_name.clone(),
461                thread_pool,
462            )?);
463        }
464
465        Ok(reports)
466    }
467
468    pub fn activate_loaded_integrand_backends(
469        &mut self,
470        allow_symjit_fallback: bool,
471    ) -> Result<()> {
472        for process in &mut self.processes {
473            process.activate_loaded_integrand_backends(allow_symjit_fallback)?;
474        }
475        Ok(())
476    }
477
478    pub fn get_integrand(
479        &self,
480        process_id: usize,
481        integrand_name: impl AsRef<str>,
482    ) -> Result<crate::processes::process::ResolvedIntegrandRef<'_>> {
483        let process = &self.processes[process_id];
484        process.get_integrand(integrand_name)
485    }
486
487    pub fn get_integrand_mut(
488        &mut self,
489        process_id: usize,
490        integrand_name: impl AsRef<str>,
491    ) -> Result<&mut crate::integrands::process::ProcessIntegrand> {
492        let process = &mut self.processes[process_id];
493        process.get_integrand_mut(integrand_name)
494    }
495
496    pub fn export_dot(&self, path: impl AsRef<Path>, settings: &DotExportSettings) -> Result<()> {
497        let path = path.as_ref().join("processes");
498        fs::create_dir_all(&path)?;
499
500        for p in self.processes.iter() {
501            p.export_dot(&path, settings)?;
502        }
503        Ok(())
504    }
505
506    pub fn export_uv_forests(
507        &self,
508        path: impl AsRef<Path>,
509        process_id: usize,
510        integrand_name: &str,
511        graph_ids: &[usize],
512        settings: &UVForestExportSettings,
513    ) -> Result<()> {
514        let path = path.as_ref().join("processes");
515        fs::create_dir_all(&path)?;
516        let process = self.processes.get(process_id).ok_or_else(|| {
517            eyre!(
518                "Process id {} is out of range; there are {} processes",
519                process_id,
520                self.processes.len()
521            )
522        })?;
523        process.export_uv_forests(&path, integrand_name, graph_ids, settings)
524    }
525
526    pub fn export_standalone(
527        &self,
528        path: impl AsRef<Path>,
529        settings: &StandaloneExportSettings,
530    ) -> Result<()> {
531        let path = path.as_ref().join("processes");
532        fs::create_dir_all(&path)?;
533
534        for p in self.processes.iter() {
535            p.export_standalone(&path, settings)?;
536        }
537        Ok(())
538    }
539
540    pub fn add_process(&mut self, process: Process) {
541        self.processes.push(process);
542    }
543
544    ///preprocesses the process list according to the settings
545    pub fn preprocess(
546        &mut self,
547        model: &Model,
548        settings: &GlobalSettings,
549        locked_runtime_settings: &LockedRuntimeSettings,
550        thread_pool: &ThreadPool,
551    ) -> Result<Vec<GeneratedGraphReport>> {
552        let mut reports = Vec::new();
553        for process in self.processes.iter_mut() {
554            reports.extend(process.preprocess(
555                model,
556                settings,
557                locked_runtime_settings,
558                thread_pool,
559            )?);
560        }
561        Ok(reports)
562    }
563
564    pub fn generate_integrands(
565        &mut self,
566        model: &Model,
567        global_settings: &GlobalSettings,
568        runtime_default: LockedRuntimeSettings,
569        thread_pool: &ThreadPool,
570    ) -> Result<Vec<GeneratedGraphReport>> {
571        let mut reports = Vec::new();
572        for process in &mut self.processes {
573            reports.extend(process.generate_integrands(
574                model,
575                global_settings,
576                runtime_default,
577                thread_pool,
578            )?);
579        }
580        Ok(reports)
581    }
582
583    pub fn find_process(&self, process_id: Option<usize>) -> Result<usize> {
584        if let Some(id) = process_id {
585            if id >= self.processes.len() {
586                return Err(color_eyre::eyre::eyre!(
587                    "Invalid process id {}. Number of processes: {}",
588                    id,
589                    self.processes.len()
590                ));
591            }
592            Ok(id)
593        } else if self.processes.is_empty() {
594            Err(color_eyre::eyre::eyre!("No processes generated yet."))
595        } else if self.processes.len() > 1 {
596            Err(color_eyre::eyre::eyre!(
597                "There are {} processes available. Please specify a process.",
598                self.processes.len()
599            ))
600        } else {
601            Ok(0)
602        }
603    }
604
605    pub fn find_integrand(
606        &self,
607        process_id: Option<usize>,
608        integrand_name: Option<&String>,
609    ) -> Result<(usize, String)> {
610        let p_id = self.find_process(process_id)?;
611        let integrand_name = self.processes[p_id]
612            .collection
613            .find_integrand(integrand_name.cloned())?;
614        Ok((p_id, integrand_name))
615    }
616}
617
618pub mod process;
619pub use process::*;
620pub mod amplitude;
621pub use amplitude::*;
622pub mod cross_section;
623pub use cross_section::*;
624
625#[cfg(test)]
626mod tests {
627    use std::fs::OpenOptions;
628
629    use linnet::half_edge::{
630        involution::EdgeIndex,
631        subgraph::{SuBitGraph, SubSetLike},
632    };
633
634    use symbolica::state::State;
635
636    use crate::{
637        GammaLoopContextContainer, dot,
638        graph::{Graph, LoopMomentumBasis, parse::IntoGraph},
639        momentum::signature::LoopExtSignature,
640        settings::{
641            RuntimeSettings,
642            global::{
643                CompilationMode, CompilationOptimizationLevel, GammaloopCompileOptions,
644                GenerationSettings, TropicalSubgraphTableSettings,
645            },
646            runtime::LockedRuntimeSettings,
647        },
648        utils::load_generic_model,
649    };
650
651    use super::AmplitudeGraph;
652
653    mod failing {
654        use super::*;
655
656        #[test]
657        fn test_encode_decode_amplitude_graph() {
658            // load the model and hack the masses, go through serializable model since arc is not mutable
659            let model = load_generic_model("sm");
660
661            let mut graph: Graph = dot!(
662                digraph G{
663                    e1      [flow=sink]
664                    e2      [flow=source]
665                    e3      [flow=source]
666                    e1 -> n1  [particle=h]
667                    e2 -> n4    [particle=h]
668                    n1 -> n2    [particle=h]
669                    n1 -> n3    [particle=h]
670                    n2 -> n3    [particle=t]
671                    n3 -> n4    [particle=t]
672                    n4 -> n2    [particle=t]
673                }
674            )
675            .unwrap();
676            let loop_momentum_basis = LoopMomentumBasis {
677                tree: SuBitGraph::empty(0),
678                loop_edges: vec![EdgeIndex::from(0), EdgeIndex::from(4)].into(),
679                ext_edges: vec![EdgeIndex::from(5), EdgeIndex::from(6)].into(),
680                edge_signatures: graph
681                    .underlying
682                    .new_edgevec(|_, _, _| LoopExtSignature::from((vec![], vec![]))),
683            };
684
685            // loop_momentum_basis
686            //     .set_edge_signatures(&graph.underlying)
687            //     .unwrap();
688
689            graph.loop_momentum_basis = loop_momentum_basis;
690
691            let mut amplitude: AmplitudeGraph = AmplitudeGraph::new(graph.clone());
692
693            amplitude
694                .preprocess(
695                    &model,
696                    &GenerationSettings {
697                        compile: GammaloopCompileOptions {
698                            compilation_mode: CompilationMode::Cpp,
699                            fast_math: false,
700                            optimization_level: CompilationOptimizationLevel::O0,
701                            unsafe_math: false,
702                            compiler: crate::settings::global::default_external_compiler()
703                                .to_owned(),
704                            custom: Vec::new(),
705                        },
706                        tropical_subgraph_table: TropicalSubgraphTableSettings {
707                            panic_on_fail: false,
708                            target_omega: 1.0,
709                            ..Default::default()
710                        },
711                        ..Default::default()
712                    },
713                    &LockedRuntimeSettings::from(&RuntimeSettings::default()),
714                )
715                .unwrap();
716
717            let mut temp = OpenOptions::new()
718                .write(true)
719                .create(true)
720                .truncate(true)
721                .open("test.bin")
722                .unwrap();
723
724            State::export(&mut temp).unwrap();
725            drop(temp);
726
727            let mut temp = OpenOptions::new().read(true).open("test.bin").unwrap();
728            let state_map = State::import(&mut temp, None).unwrap();
729
730            let context = GammaLoopContextContainer {
731                model: &model,
732                state_map: &state_map,
733            };
734
735            println!("context created");
736
737            let encoded_amplitude =
738                bincode::encode_to_vec(&amplitude, bincode::config::standard()).unwrap();
739
740            let _amplitude: AmplitudeGraph = bincode::decode_from_slice_with_context(
741                &encoded_amplitude,
742                bincode::config::standard(),
743                context,
744            )
745            .expect("amplitude decode failed")
746            .0;
747
748            println!("amplitude graph passed");
749        }
750    }
751}