From 8b5eb265df88f6a0e03e74d7cde920b228bba8c6 Mon Sep 17 00:00:00 2001 From: MiaochenJin Date: Wed, 12 Jun 2024 15:21:20 -0400 Subject: [PATCH 01/93] finished implementing D meson DB, but lower energies decay still is a bit problematic --- projects/dataclasses/private/Particle.cxx | 11 + .../public/SIREN/dataclasses/Particle.h | 3 + .../SIREN/dataclasses/ParticleTypes.def | 6 + .../SecondaryBoundedVertexDistribution.cxx | 17 + .../SecondaryPhysicalVertexDistribution.cxx | 21 + .../SecondaryVertexPositionDistribution.cxx | 1 + projects/injection/private/Injector.cxx | 202 ++++-- projects/injection/private/Weighter.cxx | 6 + .../injection/ColumnDepthLeptonInjector.h | 2 + .../injection/CylinderVolumeLeptonInjector.h | 2 + .../injection/DecayRangeLeptonInjector.h | 2 + .../SIREN/injection/RangedLeptonInjector.h | 2 + projects/interactions/CMakeLists.txt | 5 + .../private/CharmDISFromSpline.cxx | 601 ++++++++++++++++++ .../private/CharmHadronization.cxx | 166 +++++ .../interactions/private/CharmMesonDecay.cxx | 505 +++++++++++++++ projects/interactions/private/DMesonELoss.cxx | 281 ++++++++ .../interactions/private/Hadronization.cxx | 17 + .../private/InteractionCollection.cxx | 19 +- .../private/pybindings/CharmDISFromSpline.h | 87 +++ .../private/pybindings/CharmHadronization.h | 38 ++ .../private/pybindings/CharmMesonDecay.h | 34 + .../private/pybindings/DMesonELoss.h | 37 ++ .../private/pybindings/Hadronization.h | 34 + .../pybindings/InteractionCollection.h | 7 + .../private/pybindings/interactions.cxx | 17 + .../SIREN/interactions/CharmDISFromSpline.h | 162 +++++ .../SIREN/interactions/CharmHadronization.h | 82 +++ .../SIREN/interactions/CharmMesonDecay.h | 89 +++ .../public/SIREN/interactions/DMesonELoss.h | 96 +++ .../public/SIREN/interactions/Hadronization.h | 58 ++ .../interactions/InteractionCollection.h | 17 +- .../public/SIREN/utilities/Constants.h | 6 + python/SIREN_Controller.py | 2 +- 34 files changed, 2567 insertions(+), 68 deletions(-) create mode 100644 projects/interactions/private/CharmDISFromSpline.cxx create mode 100644 projects/interactions/private/CharmHadronization.cxx create mode 100644 projects/interactions/private/CharmMesonDecay.cxx create mode 100644 projects/interactions/private/DMesonELoss.cxx create mode 100644 projects/interactions/private/Hadronization.cxx create mode 100644 projects/interactions/private/pybindings/CharmDISFromSpline.h create mode 100644 projects/interactions/private/pybindings/CharmHadronization.h create mode 100644 projects/interactions/private/pybindings/CharmMesonDecay.h create mode 100644 projects/interactions/private/pybindings/DMesonELoss.h create mode 100644 projects/interactions/private/pybindings/Hadronization.h create mode 100644 projects/interactions/public/SIREN/interactions/CharmDISFromSpline.h create mode 100644 projects/interactions/public/SIREN/interactions/CharmHadronization.h create mode 100644 projects/interactions/public/SIREN/interactions/CharmMesonDecay.h create mode 100644 projects/interactions/public/SIREN/interactions/DMesonELoss.h create mode 100644 projects/interactions/public/SIREN/interactions/Hadronization.h diff --git a/projects/dataclasses/private/Particle.cxx b/projects/dataclasses/private/Particle.cxx index 60154e49d..7952adc1e 100644 --- a/projects/dataclasses/private/Particle.cxx +++ b/projects/dataclasses/private/Particle.cxx @@ -83,5 +83,16 @@ bool isCharged(ParticleType p){ p==ParticleType::Hadrons); } +bool isQuark(Particle::ParticleType p){ + return(p==Particle::ParticleType::Charm || p==Particle::ParticleType::CharmBar); + } + +bool isHadron(Particle::ParticleType p){ + return(p==Particle::ParticleType::Hadrons || + p==Particle::ParticleType::D0 || p==Particle::ParticleType::D0Bar || + p==Particle::ParticleType::DPlus || p==Particle::ParticleType::DMinus); + } + + } // namespace utilities } // namespace siren diff --git a/projects/dataclasses/public/SIREN/dataclasses/Particle.h b/projects/dataclasses/public/SIREN/dataclasses/Particle.h index 94a61693f..fff79c4a6 100644 --- a/projects/dataclasses/public/SIREN/dataclasses/Particle.h +++ b/projects/dataclasses/public/SIREN/dataclasses/Particle.h @@ -74,6 +74,9 @@ class Particle { bool isLepton(ParticleType p); bool isCharged(ParticleType p); bool isNeutrino(ParticleType p); +bool isQuark(Particle::ParticleType p); +bool isHadron(Particle::ParticleType p); + } // namespace dataclasses } // namespace siren diff --git a/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def b/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def index 8c0687c3c..73b578f97 100644 --- a/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def +++ b/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def @@ -56,6 +56,12 @@ X(TauPlus, -15) X(TauMinus, 15) X(NuTau, 16) X(NuTauBar, -16) +X(Charm, 4) +X(CharmBar, -4) +X(K0, 311) +X(K0Bar, -311) + + /* Nuclei */ X(HNucleus, 1000010010) diff --git a/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx b/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx index 2a648aac5..ca3c407cd 100644 --- a/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx +++ b/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx @@ -53,10 +53,17 @@ double log_one_minus_exp_of_negative(double x) { void SecondaryBoundedVertexDistribution::SampleVertex(std::shared_ptr rand, std::shared_ptr detector_model, std::shared_ptr interactions, siren::dataclasses::SecondaryDistributionRecord & record) const { + std::cout << "in sample bounded vertex" << std::endl; + siren::math::Vector3D pos = record.initial_position; siren::math::Vector3D dir = record.direction; siren::math::Vector3D endcap_0 = pos; + // skip computation of endpoint if interaction is hadronization + if (interactions->HasHadronizations()) { + record.SetLength(0); + return; + } siren::math::Vector3D endcap_1 = endcap_0 + max_length * dir; siren::detector::Path path(detector_model, DetectorPosition(endcap_0), DetectorDirection(dir), max_length); @@ -116,11 +123,21 @@ void SecondaryBoundedVertexDistribution::SampleVertex(std::shared_ptr detector_model, std::shared_ptr interactions, siren::dataclasses::InteractionRecord const & record) const { + std::cout << "in sample bounded vertex gen prob" << std::endl; + siren::math::Vector3D dir(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]); dir.normalize(); siren::math::Vector3D vertex(record.interaction_vertex); siren::math::Vector3D endcap_0 = record.primary_initial_position; + // hadrnoization treated differently, but also check for misconducting + if (interactions->HasHadronizations()) { + if (vertex == endcap_0) { + return 1.0; + } else { + return 0.0; + } + } siren::math::Vector3D endcap_1 = endcap_0 + max_length * dir; siren::detector::Path path(detector_model, DetectorPosition(endcap_0), DetectorDirection(dir), max_length); diff --git a/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx b/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx index 9b58ce9e7..f9fb53b41 100644 --- a/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx +++ b/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx @@ -51,10 +51,21 @@ double log_one_minus_exp_of_negative(double x) { void SecondaryPhysicalVertexDistribution::SampleVertex(std::shared_ptr rand, std::shared_ptr detector_model, std::shared_ptr interactions, siren::dataclasses::SecondaryDistributionRecord & record) const { + std::cout << "in sample physical vertex" << std::endl; siren::math::Vector3D pos = record.initial_position; siren::math::Vector3D dir = record.direction; + // std::cout << "in sample physical vertex-1" << std::endl; siren::math::Vector3D endcap_0 = pos; + // treat hadronizations differntely + if (interactions->HasHadronizations()) { + std::cout << "in sample physical vertex-hadron" << std::endl; + + record.SetLength(0); + return; + } + // std::cout << "in sample physical vertex-shouldnm't be here" << std::endl; + siren::detector::Path path(detector_model, DetectorPosition(endcap_0), DetectorDirection(dir), std::numeric_limits::infinity()); path.ClipToOuterBounds(); @@ -75,6 +86,7 @@ void SecondaryPhysicalVertexDistribution::SampleVertex(std::shared_ptr detector_model, std::shared_ptr interactions, siren::dataclasses::InteractionRecord const & record) const { + std::cout << "in sample physical vertex gen prob" << std::endl; + siren::math::Vector3D dir(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]); dir.normalize(); siren::math::Vector3D vertex(record.interaction_vertex); siren::math::Vector3D endcap_0 = record.primary_initial_position; + if (interactions->HasHadronizations()) { + if (vertex == endcap_0) { + return 1.0; + } else { + return 0.0; + } + } siren::detector::Path path(detector_model, DetectorPosition(endcap_0), DetectorDirection(dir), std::numeric_limits::infinity()); path.ClipToOuterBounds(); diff --git a/projects/distributions/private/secondary/vertex/SecondaryVertexPositionDistribution.cxx b/projects/distributions/private/secondary/vertex/SecondaryVertexPositionDistribution.cxx index 87b44e63d..18e9c0b69 100644 --- a/projects/distributions/private/secondary/vertex/SecondaryVertexPositionDistribution.cxx +++ b/projects/distributions/private/secondary/vertex/SecondaryVertexPositionDistribution.cxx @@ -15,6 +15,7 @@ namespace distributions { // class SecondaryVertexPositionDistribution : InjectionDistribution //--------------- void SecondaryVertexPositionDistribution::Sample(std::shared_ptr rand, std::shared_ptr detector_model, std::shared_ptr interactions, siren::dataclasses::SecondaryDistributionRecord & record) const { + // std::cout << "sampling vertex" << std::endl; SampleVertex(rand, detector_model, interactions, record); } diff --git a/projects/injection/private/Injector.cxx b/projects/injection/private/Injector.cxx index 877a729e6..1725b3f64 100644 --- a/projects/injection/private/Injector.cxx +++ b/projects/injection/private/Injector.cxx @@ -12,6 +12,8 @@ #include "SIREN/interactions/DarkNewsCrossSection.h" #include "SIREN/interactions/InteractionCollection.h" #include "SIREN/interactions/Decay.h" +#include "SIREN/interactions/Hadronization.h" + #include "SIREN/dataclasses/DecaySignature.h" #include "SIREN/dataclasses/InteractionSignature.h" #include "SIREN/dataclasses/Particle.h" @@ -180,109 +182,164 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record std::vector matching_signatures; std::vector> matching_cross_sections; std::vector> matching_decays; + std::vector> matching_hadronizations; + siren::dataclasses::InteractionRecord fake_record = record; double fake_prob; - if (interactions->HasCrossSections()) { - for(auto const target : available_targets) { - if(possible_targets.find(target) != possible_targets.end()) { - // Get target density - double target_density = detector_model->GetParticleDensity(intersections, DetectorPosition(interaction_vertex), target); - // Loop over cross sections that have this target - std::vector> const & target_cross_sections = interactions->GetCrossSectionsForTarget(target); - for(auto const & cross_section : target_cross_sections) { - // Loop over cross section signatures with the same target - std::vector signatures = cross_section->GetPossibleSignaturesFromParents(record.signature.primary_type, target); - for(auto const & signature : signatures) { - fake_record.signature = signature; - fake_record.target_mass = detector_model->GetTargetMass(target); - // Add total cross section times density to the total prob - fake_prob = target_density * cross_section->TotalCrossSection(fake_record); - total_prob += fake_prob; - xsec_prob += fake_prob; - // Add total prob to probs - probs.push_back(total_prob); - // Add target and cross section pointer to the lists - matching_targets.push_back(target); - matching_cross_sections.push_back(cross_section); - matching_signatures.push_back(signature); + // if contains hadronization, then perform only hadronization + if (interactions->HasHadronizations()) { + std::cout << "saw hadronization" << std::endl; + double total_frag_prob = 0; + std::vector frag_probs; + for(auto const & hadronization : interactions->GetHadronizations() ) { + for(auto const & signature : hadronization->GetPossibleSignaturesFromParent(record.signature.primary_type)) { + double frag_prob = 0; + + fake_record.signature = signature; + for (auto & secondary : fake_record.signature.secondary_types) { + frag_prob += hadronization->FragmentationFraction(secondary); + } + + total_frag_prob += frag_prob; + frag_probs.push_back(total_frag_prob); + // Add target and decay pointer to the lists + matching_targets.push_back(siren::dataclasses::Particle::ParticleType::Decay); + matching_hadronizations.push_back(hadronization); + matching_signatures.push_back(signature); + } + } + + std::cout << "Hadronization finished signatures" << std::endl; + + + // now choose the specific charmed hadron to fragment into + double r = random->Uniform(0, total_frag_prob); + unsigned int index = 0; + for(; (index < frag_probs.size()-1) and (r > frag_probs[index]); ++index) { + } // fixes the index of the chosen fragmentation + record.signature.target_type = matching_targets[index]; + record.signature = matching_signatures[index]; + record.target_mass = detector_model->GetTargetMass(record.signature.target_type); + siren::dataclasses::CrossSectionDistributionRecord xsec_record(record); + + matching_hadronizations[index]->SampleFinalState(xsec_record, random); + xsec_record.Finalize(record); + + std::cout << "hadronization done!" << std::endl; + + } else { + if (interactions->HasCrossSections()) { + std::cout << "saw xsec" << std::endl; + for(auto const target : available_targets) { + if(possible_targets.find(target) != possible_targets.end()) { + // Get target density + double target_density = detector_model->GetParticleDensity(intersections, DetectorPosition(interaction_vertex), target); + // Loop over cross sections that have this target + std::vector> const & target_cross_sections = interactions->GetCrossSectionsForTarget(target); + for(auto const & cross_section : target_cross_sections) { + // Loop over cross section signatures with the same target + std::vector signatures = cross_section->GetPossibleSignaturesFromParents(record.signature.primary_type, target); + for(auto const & signature : signatures) { + fake_record.signature = signature; + fake_record.target_mass = detector_model->GetTargetMass(target); + // Add total cross section times density to the total prob + fake_prob = target_density * cross_section->TotalCrossSection(fake_record); + total_prob += fake_prob; + xsec_prob += fake_prob; + // Add total prob to probs + probs.push_back(total_prob); + // Add target and cross section pointer to the lists + matching_targets.push_back(target); + matching_cross_sections.push_back(cross_section); + matching_signatures.push_back(signature); + } } } } } - } - if (interactions->HasDecays()) { - for(auto const & decay : interactions->GetDecays() ) { - for(auto const & signature : decay->GetPossibleSignaturesFromParent(record.signature.primary_type)) { - fake_record.signature = signature; - // fake_prob has units of 1/cm to match cross section probabilities - fake_prob = 1./(decay->TotalDecayLengthForFinalState(fake_record)/siren::utilities::Constants::cm); - total_prob += fake_prob; - // Add total prob to probs - probs.push_back(total_prob); - // Add target and decay pointer to the lists - matching_targets.push_back(siren::dataclasses::ParticleType::Decay); - matching_decays.push_back(decay); - matching_signatures.push_back(signature); + if (interactions->HasDecays()) { + std::cout << "saw decay" << std::endl; + for(auto const & decay : interactions->GetDecays() ) { + for(auto const & signature : decay->GetPossibleSignaturesFromParent(record.signature.primary_type)) { + fake_record.signature = signature; + // fake_prob has units of 1/cm to match cross section probabilities + fake_prob = 1./(decay->TotalDecayLengthForFinalState(fake_record)/siren::utilities::Constants::cm); + total_prob += fake_prob; + // Add total prob to probs + probs.push_back(total_prob); + // Add target and decay pointer to the lists + matching_targets.push_back(siren::dataclasses::ParticleType::Decay); + matching_decays.push_back(decay); + matching_signatures.push_back(signature); + } } } - } - if(total_prob == 0) - throw(siren::utilities::InjectionFailure("No valid interactions for this event!")); - // Throw a random number - double r = random->Uniform(0, total_prob); - // Choose the target and cross section - unsigned int index = 0; - for(; (index+1 < probs.size()) and (r > probs[index]); ++index) {} - record.signature.target_type = matching_targets[index]; - record.signature = matching_signatures[index]; - double selected_prob = 0.0; - for(unsigned int i=0; i 0 ? probs[i] - probs[i - 1] : probs[i]); + if(total_prob == 0) + throw(siren::utilities::InjectionFailure("No valid interactions for this event!")); + // Throw a random number + double r = random->Uniform(0, total_prob); + // Choose the target and cross section + unsigned int index = 0; + for(; (index+1 < probs.size()) and (r > probs[index]); ++index) {} + record.signature.target_type = matching_targets[index]; + record.signature = matching_signatures[index]; + double selected_prob = 0.0; + for(unsigned int i=0; i 0 ? probs[i] - probs[i - 1] : probs[i]); + } } + if(selected_prob == 0) + throw(siren::utilities::InjectionFailure("No valid interactions for this event!")); + record.target_mass = detector_model->GetTargetMass(record.signature.target_type); + siren::dataclasses::CrossSectionDistributionRecord xsec_record(record); + if(r <= xsec_prob) { + matching_cross_sections[index]->SampleFinalState(xsec_record, random); + } else { + matching_decays[index - matching_cross_sections.size()]->SampleFinalState(xsec_record, random); + } + xsec_record.Finalize(record); } - if(selected_prob == 0) - throw(siren::utilities::InjectionFailure("No valid interactions for this event!")); - record.target_mass = detector_model->GetTargetMass(record.signature.target_type); - siren::dataclasses::CrossSectionDistributionRecord xsec_record(record); - if(r <= xsec_prob) { - matching_cross_sections[index]->SampleFinalState(xsec_record, random); - } else { - matching_decays[index - matching_cross_sections.size()]->SampleFinalState(xsec_record, random); - } - xsec_record.Finalize(record); } // Function to sample secondary processes // // Modifies an InteractionRecord with the new event siren::dataclasses::InteractionRecord Injector::SampleSecondaryProcess(siren::dataclasses::SecondaryDistributionRecord & secondary_record) const { + std::cout << "sampling secondary" << std::endl; + std::cout << "secondary record type is " << secondary_record.type << " " << secondary_record.id << std::endl; std::shared_ptr secondary_process = secondary_process_map.at(secondary_record.type); std::shared_ptr secondary_interactions = secondary_process->GetInteractions(); std::vector> secondary_distributions = secondary_process->GetSecondaryInjectionDistributions(); - size_t max_tries = 100; + size_t max_tries = 10; size_t tries = 0; size_t failed_tries = 0; while(true) { + // std::cout << "gotcha" << std::endl; try { for(auto & distribution : secondary_distributions) { + std::cout << "sample distribution" << std::endl; distribution->Sample(random, detector_model, secondary_process->GetInteractions(), secondary_record); } siren::dataclasses::InteractionRecord record; secondary_record.Finalize(record); + // std::cout << "sample distribution" << std::endl; + SampleCrossSection(record, secondary_interactions); return record; } catch(siren::utilities::InjectionFailure const & e) { + std::cout << "caught error" << std::endl; + failed_tries += 1; - if(tries > max_tries) { + if(failed_tries > max_tries) { throw(siren::utilities::InjectionFailure("Failed to generate secondary process!")); break; } continue; } - if(tries > max_tries) { + if(failed_tries > max_tries) { throw(siren::utilities::InjectionFailure("Failed to generate secondary process!")); break; } @@ -292,10 +349,11 @@ siren::dataclasses::InteractionRecord Injector::SampleSecondaryProcess(siren::da siren::dataclasses::InteractionTree Injector::GenerateEvent() { siren::dataclasses::InteractionRecord record; - size_t max_tries = 100; + size_t max_tries = 10; size_t tries = 0; size_t failed_tries = 0; // Initial Process + // std::cout << "Sampling primary interactions" << std::endl; while(true) { tries += 1; try { @@ -323,9 +381,12 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { std::shared_ptr parent = tree.add_entry(record); // Secondary Processes + // std::cout << "Sampling primary interactions" << std::endl; + std::deque, std::shared_ptr>> secondaries; std::function)> add_secondaries = [&](std::shared_ptr parent) { for(size_t i=0; irecord.signature.secondary_types.size(); ++i) { + // std::cout << "for loop 1" << std::endl; siren::dataclasses::ParticleType const & type = parent->record.signature.secondary_types[i]; std::map>::iterator it = secondary_process_map.find(type); if(it == secondary_process_map.end()) { @@ -342,16 +403,27 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { }; add_secondaries(parent); + // std::cout << "num secondaries: " << secondaries.size() << std::endl; while(secondaries.size() > 0) { + // std::cout << "while loop 1" << std::endl; + for(int i = secondaries.size() - 1; i >= 0; --i) { + // std::cout << "for loop 2" << std::endl; + std::shared_ptr parent = std::get<0>(secondaries[i]); std::shared_ptr secondary_dist = std::get<1>(secondaries[i]); + // std::cout << "for loop 2-1" << std::endl; + secondaries.erase(secondaries.begin() + i); siren::dataclasses::InteractionRecord secondary_record = SampleSecondaryProcess(*secondary_dist); std::shared_ptr secondary_datum = tree.add_entry(secondary_record, parent); + // std::cout << "for loop 2-2" << std::endl; + add_secondaries(secondary_datum); } + // std::cout << "while loop 1-2" << std::endl; + } injected_events += 1; return tree; diff --git a/projects/injection/private/Weighter.cxx b/projects/injection/private/Weighter.cxx index 81dbe6dfb..afd11cfb8 100644 --- a/projects/injection/private/Weighter.cxx +++ b/projects/injection/private/Weighter.cxx @@ -25,6 +25,8 @@ #include "SIREN/injection/Process.h" // for Phy... #include "SIREN/injection/WeightingUtils.h" // for Cro... #include "SIREN/math/Vector3D.h" // for Vec... +#include "SIREN/dataclasses/Particle.h" + #include "SIREN/injection/Injector.h" @@ -111,6 +113,10 @@ double Weighter::EventWeight(siren::dataclasses::InteractionTree const & tree) c double physical_probability = 1.0; double generation_probability = injectors[idx]->EventsToInject();//GenerationProbability(tree); for(auto const & datum : tree.tree) { + // skip weighting if record contains hadronization + if (isQuark(datum->record.signature.primary_type)) { + continue; + } std::tuple bounds; if(datum->depth() == 0) { bounds = injectors[idx]->PrimaryInjectionBounds(datum->record); diff --git a/projects/injection/public/SIREN/injection/ColumnDepthLeptonInjector.h b/projects/injection/public/SIREN/injection/ColumnDepthLeptonInjector.h index 73d541132..9caa13eb7 100644 --- a/projects/injection/public/SIREN/injection/ColumnDepthLeptonInjector.h +++ b/projects/injection/public/SIREN/injection/ColumnDepthLeptonInjector.h @@ -22,6 +22,8 @@ #include "SIREN/interactions/CrossSection.h" #include "SIREN/interactions/Decay.h" +#include "SIREN/interactions/Hadronization.h" + #include "SIREN/detector/DetectorModel.h" #include "SIREN/distributions/primary/vertex/ColumnDepthPositionDistribution.h" #include "SIREN/distributions/primary/vertex/DepthFunction.h" diff --git a/projects/injection/public/SIREN/injection/CylinderVolumeLeptonInjector.h b/projects/injection/public/SIREN/injection/CylinderVolumeLeptonInjector.h index 4cb2bd3d5..1aabfd1f5 100644 --- a/projects/injection/public/SIREN/injection/CylinderVolumeLeptonInjector.h +++ b/projects/injection/public/SIREN/injection/CylinderVolumeLeptonInjector.h @@ -23,6 +23,8 @@ #include "SIREN/interactions/InteractionCollection.h" #include "SIREN/interactions/CrossSection.h" #include "SIREN/interactions/Decay.h" +#include "SIREN/interactions/Hadronization.h" + #include "SIREN/detector/DetectorModel.h" #include "SIREN/distributions/primary/vertex/CylinderVolumePositionDistribution.h" #include "SIREN/geometry/Cylinder.h" // for Cylinder diff --git a/projects/injection/public/SIREN/injection/DecayRangeLeptonInjector.h b/projects/injection/public/SIREN/injection/DecayRangeLeptonInjector.h index 26a0c5ec4..d2eb44ded 100644 --- a/projects/injection/public/SIREN/injection/DecayRangeLeptonInjector.h +++ b/projects/injection/public/SIREN/injection/DecayRangeLeptonInjector.h @@ -23,6 +23,8 @@ #include "SIREN/interactions/InteractionCollection.h" #include "SIREN/interactions/CrossSection.h" #include "SIREN/interactions/Decay.h" +#include "SIREN/interactions/Hadronization.h" + #include "SIREN/detector/DetectorModel.h" #include "SIREN/distributions/primary/vertex/DecayRangeFunction.h" #include "SIREN/distributions/primary/vertex/DecayRangePositionDistribution.h" diff --git a/projects/injection/public/SIREN/injection/RangedLeptonInjector.h b/projects/injection/public/SIREN/injection/RangedLeptonInjector.h index 581aa91ae..a835b9c45 100644 --- a/projects/injection/public/SIREN/injection/RangedLeptonInjector.h +++ b/projects/injection/public/SIREN/injection/RangedLeptonInjector.h @@ -21,6 +21,8 @@ #include "SIREN/interactions/InteractionCollection.h" #include "SIREN/interactions/CrossSection.h" #include "SIREN/interactions/Decay.h" +#include "SIREN/interactions/Hadronization.h" + #include "SIREN/detector/DetectorModel.h" #include "SIREN/distributions/primary/vertex/RangeFunction.h" #include "SIREN/distributions/primary/vertex/RangePositionDistribution.h" diff --git a/projects/interactions/CMakeLists.txt b/projects/interactions/CMakeLists.txt index c6ee384d2..a8eec569d 100644 --- a/projects/interactions/CMakeLists.txt +++ b/projects/interactions/CMakeLists.txt @@ -16,6 +16,11 @@ LIST (APPEND interactions_SOURCES ${PROJECT_SOURCE_DIR}/projects/interactions/private/DummyCrossSection.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/pyDarkNewsCrossSection.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/pyDarkNewsDecay.cxx + ${PROJECT_SOURCE_DIR}/projects/interactions/private/CharmDISFromSpline.cxx + ${PROJECT_SOURCE_DIR}/projects/interactions/private/Hadronization.cxx + ${PROJECT_SOURCE_DIR}/projects/interactions/private/CharmHadronization.cxx + ${PROJECT_SOURCE_DIR}/projects/interactions/private/CharmMesonDecay.cxx + ${PROJECT_SOURCE_DIR}/projects/interactions/private/DMesonELoss.cxx ) add_library(SIREN_interactions OBJECT ${interactions_SOURCES}) set_property(TARGET SIREN_interactions PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/projects/interactions/private/CharmDISFromSpline.cxx b/projects/interactions/private/CharmDISFromSpline.cxx new file mode 100644 index 000000000..45f15b98e --- /dev/null +++ b/projects/interactions/private/CharmDISFromSpline.cxx @@ -0,0 +1,601 @@ +#include "SIREN/interactions/CharmDISFromSpline.h" + +#include // for map, opera... +#include // for set, opera... +#include // for array +#include // for pow, log10 +#include // for tie, opera... +#include // for allocator +#include // for basic_string +#include // for vector +#include // for assert +#include // for size_t + +#include // for P4, Boost +#include // for Vector3 + +#include // for splinetable +//#include + +#include "SIREN/interactions/CrossSection.h" // for CrossSection +#include "SIREN/dataclasses/InteractionRecord.h" // for Interactio... +#include "SIREN/dataclasses/Particle.h" // for Particle +#include "SIREN/utilities/Random.h" // for SIREN_random +#include "SIREN/utilities/Constants.h" // for electronMass + +namespace siren { +namespace interactions { + +namespace { +///Check whether a given point in phase space is physically realizable. +///Based on equations 6-8 of http://dx.doi.org/10.1103/PhysRevD.66.113007 +///S. Kretzer and M. H. Reno +///"Tau neutrino deep inelastic charged current interactions" +///Phys. Rev. D 66, 113007 +///\param x Bjorken x of the interaction +///\param y Bjorken y of the interaction +///\param E Incoming neutrino in energy in the lab frame ($E_\nu$) +///\param M Mass of the target nucleon ($M_N$) +///\param m Mass of the secondary lepton ($m_\tau$) +bool kinematicallyAllowed(double x, double y, double E, double M, double m) { + if(x > 1) //Eq. 6 right inequality + return false; + if(x < ((m * m) / (2 * M * (E - m)))) //Eq. 6 left inequality + return false; + //denominator of a and b + double d = 2 * (1 + (M * x) / (2 * E)); + //the numerator of a (or a*d) + double ad = 1 - m * m * ((1 / (2 * M * E * x)) + (1 / (2 * E * E))); + double term = 1 - ((m * m) / (2 * M * E * x)); + //the numerator of b (or b*d) + double bd = sqrt(term * term - ((m * m) / (E * E))); + return (ad - bd) <= d * y and d * y <= (ad + bd); //Eq. 7 +} +} + +CharmDISFromSpline::CharmDISFromSpline() {} + +CharmDISFromSpline::CharmDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { + LoadFromMemory(differential_data, total_data); + InitializeSignatures(); + SetUnits(units); +} + +CharmDISFromSpline::CharmDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { + LoadFromMemory(differential_data, total_data); + InitializeSignatures(); + SetUnits(units); +} + +CharmDISFromSpline::CharmDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { + LoadFromFile(differential_filename, total_filename); + InitializeSignatures(); + SetUnits(units); +} + +CharmDISFromSpline::CharmDISFromSpline(std::string differential_filename, std::string total_filename, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types) { + LoadFromFile(differential_filename, total_filename); + ReadParamsFromSplineTable(); + InitializeSignatures(); + SetUnits(units); +} + +CharmDISFromSpline::CharmDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { + LoadFromFile(differential_filename, total_filename); + InitializeSignatures(); + SetUnits(units); +} + +CharmDISFromSpline::CharmDISFromSpline(std::string differential_filename, std::string total_filename, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()) { + LoadFromFile(differential_filename, total_filename); + ReadParamsFromSplineTable(); + InitializeSignatures(); + SetUnits(units); +} + +void CharmDISFromSpline::SetUnits(std::string units) { + std::transform(units.begin(), units.end(), units.begin(), + [](unsigned char c){ return std::tolower(c); }); + if(units == "cm") { + unit = 1.0; + } else if(units == "m") { + unit = 10000.0; + } else { + throw std::runtime_error("Cross section units not supported!"); + } +} + +bool CharmDISFromSpline::equal(CrossSection const & other) const { + const CharmDISFromSpline* x = dynamic_cast(&other); + + if(!x) + return false; + else + return + std::tie( + interaction_type_, + target_mass_, + minimum_Q2_, + signatures_, + primary_types_, + target_types_, + differential_cross_section_, + total_cross_section_) + == + std::tie( + x->interaction_type_, + x->target_mass_, + x->minimum_Q2_, + x->signatures_, + x->primary_types_, + x->target_types_, + x->differential_cross_section_, + x->total_cross_section_); +} + +void CharmDISFromSpline::LoadFromFile(std::string dd_crossSectionFile, std::string total_crossSectionFile) { + + differential_cross_section_ = photospline::splinetable<>(dd_crossSectionFile.c_str()); + + if(differential_cross_section_.get_ndim()!=3 && differential_cross_section_.get_ndim()!=2) + throw std::runtime_error("cross section spline has " + std::to_string(differential_cross_section_.get_ndim()) + + " dimensions, should have either 3 (log10(E), log10(x), log10(y)) or 2 (log10(E), log10(y))"); + + total_cross_section_ = photospline::splinetable<>(total_crossSectionFile.c_str()); + + if(total_cross_section_.get_ndim() != 1) + throw std::runtime_error("Total cross section spline has " + std::to_string(total_cross_section_.get_ndim()) + + " dimensions, should have 1, log10(E)"); +} + +void CharmDISFromSpline::LoadFromMemory(std::vector & differential_data, std::vector & total_data) { + differential_cross_section_.read_fits_mem(differential_data.data(), differential_data.size()); + total_cross_section_.read_fits_mem(total_data.data(), total_data.size()); +} + +double CharmDISFromSpline::GetLeptonMass(siren::dataclasses::ParticleType lepton_type) { + int32_t lepton_number = std::abs(static_cast(lepton_type)); + double lepton_mass; + switch(lepton_number) { + case 11: + lepton_mass = siren::utilities::Constants::electronMass; + break; + case 13: + lepton_mass = siren::utilities::Constants::muonMass; + break; + case 15: + lepton_mass = siren::utilities::Constants::tauMass; + break; + case 12: + lepton_mass = 0; + case 14: + lepton_mass = 0; + case 16: + lepton_mass = 0; + break; + default: + throw std::runtime_error("Unknown lepton type!"); + } + return lepton_mass; +} + +void CharmDISFromSpline::ReadParamsFromSplineTable() { + // returns true if successfully read target mass + bool mass_good = differential_cross_section_.read_key("TARGETMASS", target_mass_); + // returns true if successfully read interaction type + bool int_good = differential_cross_section_.read_key("INTERACTION", interaction_type_); + // returns true if successfully read minimum Q2 + bool q2_good = differential_cross_section_.read_key("Q2MIN", minimum_Q2_); + + if(!int_good) { + // assume DIS to preserve compatability with previous versions + interaction_type_ = 1; + } + + if(!q2_good) { + // assume 1 GeV^2 + minimum_Q2_ = 1; + } + + if(!mass_good) { + if(int_good) { + if(interaction_type_ == 1 or interaction_type_ == 2) { + target_mass_ = (siren::dataclasses::isLepton(siren::dataclasses::ParticleType::PPlus)+ + siren::dataclasses::isLepton(siren::dataclasses::ParticleType::Neutron))/2; + } else if(interaction_type_ == 3) { + target_mass_ = siren::dataclasses::isLepton(siren::dataclasses::ParticleType::EMinus); + } else { + throw std::runtime_error("Logic error. Interaction type is not 1, 2, or 3!"); + } + + } else { + if(differential_cross_section_.get_ndim() == 3) { + target_mass_ = (siren::dataclasses::isLepton(siren::dataclasses::ParticleType::PPlus)+ + siren::dataclasses::isLepton(siren::dataclasses::ParticleType::Neutron))/2; + } else if(differential_cross_section_.get_ndim() == 2) { + target_mass_ = siren::dataclasses::isLepton(siren::dataclasses::ParticleType::EMinus); + } else { + throw std::runtime_error("Logic error. Spline dimensionality is not 2, or 3!"); + } + } + } +} + +void CharmDISFromSpline::InitializeSignatures() { + signatures_.clear(); + for(auto primary_type : primary_types_) { + dataclasses::InteractionSignature signature; + signature.primary_type = primary_type; + + if(not isNeutrino(primary_type)) { + throw std::runtime_error("This DIS implementation only supports neutrinos as primaries!"); + } + + siren::dataclasses::ParticleType charged_lepton_product = siren::dataclasses::ParticleType::unknown; + siren::dataclasses::ParticleType neutral_lepton_product = primary_type; + + if(primary_type == siren::dataclasses::ParticleType::NuE) { + charged_lepton_product = siren::dataclasses::ParticleType::EMinus; + } else if(primary_type == siren::dataclasses::ParticleType::NuEBar) { + charged_lepton_product = siren::dataclasses::ParticleType::EPlus; + } else if(primary_type == siren::dataclasses::ParticleType::NuMu) { + charged_lepton_product = siren::dataclasses::ParticleType::MuMinus; + } else if(primary_type == siren::dataclasses::ParticleType::NuMuBar) { + charged_lepton_product = siren::dataclasses::ParticleType::MuPlus; + } else if(primary_type == siren::dataclasses::ParticleType::NuTau) { + charged_lepton_product = siren::dataclasses::ParticleType::TauMinus; + } else if(primary_type == siren::dataclasses::ParticleType::NuTauBar) { + charged_lepton_product = siren::dataclasses::ParticleType::TauPlus; + } else { + throw std::runtime_error("InitializeSignatures: Unkown parent neutrino type!"); + } + + if(interaction_type_ == 1) { + signature.secondary_types.push_back(charged_lepton_product); + } else if(interaction_type_ == 2) { + signature.secondary_types.push_back(neutral_lepton_product); + } else if(interaction_type_ == 3) { + signature.secondary_types.push_back(siren::dataclasses::ParticleType::Hadrons); + } else { + throw std::runtime_error("InitializeSignatures: Unkown interaction type!"); + } + + signature.secondary_types.push_back(siren::dataclasses::ParticleType::Charm); + for(auto target_type : target_types_) { + signature.target_type = target_type; + + signatures_.push_back(signature); + + std::pair key(primary_type, target_type); + signatures_by_parent_types_[key].push_back(signature); + } + } +} + +double CharmDISFromSpline::TotalCrossSection(dataclasses::InteractionRecord const & interaction) const { + siren::dataclasses::ParticleType primary_type = interaction.signature.primary_type; + rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); + double primary_energy; + primary_energy = interaction.primary_momentum[0]; + // if we are below threshold, return 0 + if(primary_energy < InteractionThreshold(interaction)) + return 0; + return TotalCrossSection(primary_type, primary_energy); +} + +double CharmDISFromSpline::TotalCrossSection(siren::dataclasses::ParticleType primary_type, double primary_energy) const { + if(not primary_types_.count(primary_type)) { + throw std::runtime_error("Supplied primary not supported by cross section!"); + } + double log_energy = log10(primary_energy); + + if(log_energy < total_cross_section_.lower_extent(0) + or log_energy > total_cross_section_.upper_extent(0)) { + throw std::runtime_error("Interaction energy ("+ std::to_string(primary_energy) + + ") out of cross section table range: [" + + std::to_string(pow(10.,total_cross_section_.lower_extent(0))) + " GeV," + + std::to_string(pow(10.,total_cross_section_.upper_extent(0))) + " GeV]"); + } + + int center; + total_cross_section_.searchcenters(&log_energy, ¢er); + double log_xs = total_cross_section_.ndsplineeval(&log_energy, ¢er, 0); + + return unit * std::pow(10.0, log_xs); +} + +double CharmDISFromSpline::DifferentialCrossSection(dataclasses::InteractionRecord const & interaction) const { + rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); + rk::P4 p2(geom3::Vector3(0, 0, 0), interaction.target_mass); + double primary_energy; + primary_energy = interaction.primary_momentum[0]; + assert(interaction.signature.secondary_types.size() == 2); + unsigned int lepton_index = (isLepton(interaction.signature.secondary_types[0])) ? 0 : 1; + unsigned int other_index = 1 - lepton_index; + + std::array const & mom3 = interaction.secondary_momenta[lepton_index]; + std::array const & mom4 = interaction.secondary_momenta[other_index]; + rk::P4 p3(geom3::Vector3(mom3[1], mom3[2], mom3[3]), interaction.secondary_masses[lepton_index]); + rk::P4 p4(geom3::Vector3(mom4[1], mom4[2], mom4[3]), interaction.secondary_masses[other_index]); + + rk::P4 q = p1 - p3; + + double Q2 = -q.dot(q); + double y = 1.0 - p2.dot(p3) / p2.dot(p1); + double x = Q2 / (2.0 * p2.dot(q)); + double lepton_mass = GetLeptonMass(interaction.signature.secondary_types[lepton_index]); + + return DifferentialCrossSection(primary_energy, x, y, lepton_mass, Q2); +} + +double CharmDISFromSpline::DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2) const { + double log_energy = log10(energy); + // check preconditions + if(log_energy < differential_cross_section_.lower_extent(0) + || log_energy>differential_cross_section_.upper_extent(0)) + return 0.0; + if(x <= 0 || x >= 1) + return 0.0; + if(y <= 0 || y >= 1) + return 0.0; + + // we assume that: + // the target is stationary so its energy is just its mass + // the incoming neutrino is massless, so its kinetic energy is its total energy + if(std::isnan(Q2)) { + Q2 = 2.0 * energy * target_mass_ * x * y; + } + if(Q2 < minimum_Q2_) // cross section not calculated, assumed to be zero + return 0; + + // cross section should be zero, but this check is missing from the original + // CSMS calculation, so we must add it here + if(!kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) + return 0; + + std::array coordinates{{log_energy, log10(x), log10(y)}}; + std::array centers; + if(!differential_cross_section_.searchcenters(coordinates.data(), centers.data())) + return 0; + double result = pow(10., differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0)); + assert(result >= 0); + return unit * result; +} + +double CharmDISFromSpline::InteractionThreshold(dataclasses::InteractionRecord const & interaction) const { + // Consider implementing DIS thershold at some point + return 0; +} + +void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { + // Uses Metropolis-Hastings Algorithm! + // useful for cases where we don't know the supremum of our distribution, and the distribution is multi-dimensional + if (differential_cross_section_.get_ndim() != 3) { + throw std::runtime_error("I expected 3 dimensions in the cross section spline, but got " + std::to_string(differential_cross_section_.get_ndim()) +". Maybe your fits file doesn't have the right 'INTERACTION' key?"); + } + + rk::P4 p1(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); + rk::P4 p2(geom3::Vector3(0, 0, 0), record.target_mass); + + // we assume that: + // the target is stationary so its energy is just its mass + // the incoming neutrino is massless, so its kinetic energy is its total energy + // double s = target_mass_ * trecord.secondary_momentarget_mass_ + 2 * target_mass_ * primary_energy; + // double s = std::pow(rk::invMass(p1, p2), 2); + + double primary_energy; + rk::P4 p1_lab; + rk::P4 p2_lab; + p1_lab = p1; + p2_lab = p2; + primary_energy = p1_lab.e(); + + unsigned int lepton_index = (isLepton(record.signature.secondary_types[0])) ? 0 : 1; + unsigned int other_index = 1 - lepton_index; + double m = GetLeptonMass(record.signature.secondary_types[lepton_index]); + + double m1 = record.primary_mass; + double m3 = m; + double E1_lab = p1_lab.e(); + double E2_lab = p2_lab.e(); + + + // The out-going particle always gets at least enough energy for its rest mass + double yMax = 1 - m / primary_energy; + double logYMax = log10(yMax); + + // The minimum allowed value of y occurs when x = 1 and Q is minimized + double yMin = minimum_Q2_ / (2 * E1_lab * E2_lab); + double logYMin = log10(yMin); + // The minimum allowed value of x occurs when y = yMax and Q is minimized + // double xMin = minimum_Q2_ / ((s - target_mass_ * target_mass_) * yMax); + double xMin = minimum_Q2_ / (2 * E1_lab * E2_lab * yMax); + double logXMin = log10(xMin); + + bool accept; + + // kin_vars and its twin are 3-vectors containing [nu-energy, Bjorken X, Bjorken Y] + std::array kin_vars, test_kin_vars; + + // centers of the cross section spline tales. + std::array spline_table_center, test_spline_table_center; + + // values of cross_section from the splines. By * Bx * Spline(E,x,y) + double cross_section, test_cross_section; + + // No matter what, we're evaluating at this specific energy. + kin_vars[0] = test_kin_vars[0] = log10(primary_energy); + + // check preconditions + if(kin_vars[0] < differential_cross_section_.lower_extent(0) + || kin_vars[0] > differential_cross_section_.upper_extent(0)) + throw std::runtime_error("Interaction energy out of cross section table range: [" + + std::to_string(pow(10.,differential_cross_section_.lower_extent(0))) + " GeV," + + std::to_string(pow(10.,differential_cross_section_.upper_extent(0))) + " GeV]"); + + // sample an intial point + do { + // rejection sample a point which is kinematically allowed by calculation limits + double trialQ; + do { + kin_vars[1] = random->Uniform(logXMin,0); + kin_vars[2] = random->Uniform(logYMin,logYMax); + trialQ = (2 * E1_lab * E2_lab) * pow(10., kin_vars[1] + kin_vars[2]); + } while(trialQ differential_cross_section_.upper_extent(1)) { + accept = false; + } + if(kin_vars[2] < differential_cross_section_.lower_extent(2) + || kin_vars[2] > differential_cross_section_.upper_extent(2)) { + accept = false; + } + + if(accept) { + // finds the centers in the cross section spline table, returns true if it's successful + // also sets the centers + accept = differential_cross_section_.searchcenters(kin_vars.data(),spline_table_center.data()); + } + } while(!accept); + + //TODO: better proposal distribution? + double measure = pow(10., kin_vars[1] + kin_vars[2]); // Bx * By + + // Bx * By * xs(E, x, y) + // evalutates the differential spline at that point + cross_section = measure*pow(10., differential_cross_section_.ndsplineeval(kin_vars.data(), spline_table_center.data(), 0)); + + // this is the magic part. Metropolis Hastings Algorithm. + // MCMC method! + const size_t burnin = 40; // converges to the correct distribution over multiple samplings. + // big number means more accurate, but slower + for(size_t j = 0; j <= burnin; j++) { + // repeat the sampling from above to get a new valid point + double trialQ; + do { + test_kin_vars[1] = random->Uniform(logXMin, 0); + test_kin_vars[2] = random->Uniform(logYMin, logYMax); + trialQ = (2 * E1_lab * E2_lab) * pow(10., test_kin_vars[1] + test_kin_vars[2]); + } while(trialQ < minimum_Q2_ || !kinematicallyAllowed(pow(10., test_kin_vars[1]), pow(10., test_kin_vars[2]), primary_energy, target_mass_, m)); + + accept = true; + if(test_kin_vars[1] < differential_cross_section_.lower_extent(1) + || test_kin_vars[1] > differential_cross_section_.upper_extent(1)) + accept = false; + if(test_kin_vars[2] < differential_cross_section_.lower_extent(2) + || test_kin_vars[2] > differential_cross_section_.upper_extent(2)) + accept = false; + if(!accept) + continue; + + accept = differential_cross_section_.searchcenters(test_kin_vars.data(), test_spline_table_center.data()); + if(!accept) + continue; + + double measure = pow(10., test_kin_vars[1] + test_kin_vars[2]); + double eval = differential_cross_section_.ndsplineeval(test_kin_vars.data(), test_spline_table_center.data(), 0); + if(std::isnan(eval)) + continue; + test_cross_section = measure * pow(10., eval); + + double odds = (test_cross_section / cross_section); + accept = (cross_section == 0 || (odds > 1.) || random->Uniform(0, 1) < odds); + + if(accept) { + kin_vars = test_kin_vars; + cross_section = test_cross_section; + } + } + double final_x = pow(10., kin_vars[1]); + double final_y = pow(10., kin_vars[2]); + + record.interaction_parameters.clear(); + record.interaction_parameters["energy"] = E1_lab; + record.interaction_parameters["bjorken_x"] = final_x; + record.interaction_parameters["bjorken_y"] = final_y; + + double Q2 = 2 * E1_lab * E2_lab * pow(10.0, kin_vars[1] + kin_vars[2]); + double p1x_lab = std::sqrt(p1_lab.px() * p1_lab.px() + p1_lab.py() * p1_lab.py() + p1_lab.pz() * p1_lab.pz()); + double pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); + double momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); + double pqy_lab = std::sqrt(momq_lab*momq_lab - pqx_lab *pqx_lab); + double Eq_lab = E1_lab * final_y; + + geom3::UnitVector3 x_dir = geom3::UnitVector3::xAxis(); + geom3::Vector3 p1_mom = p1_lab.momentum(); + geom3::UnitVector3 p1_lab_dir = p1_mom.direction(); + geom3::Rotation3 x_to_p1_lab_rot = geom3::rotationBetween(x_dir, p1_lab_dir); + + double phi = random->Uniform(0, 2.0 * M_PI); + geom3::Rotation3 rand_rot(p1_lab_dir, phi); + + rk::P4 pq_lab(Eq_lab, geom3::Vector3(pqx_lab, pqy_lab, 0)); + pq_lab.rotate(x_to_p1_lab_rot); + pq_lab.rotate(rand_rot); + + rk::P4 p3_lab((p1_lab - pq_lab).momentum(), m3); + rk::P4 p4_lab = p2_lab + pq_lab; + + rk::P4 p3; + rk::P4 p4; + p3 = p3_lab; + p4 = p4_lab; + + std::vector & secondaries = record.GetSecondaryParticleRecords(); + siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[lepton_index]; + siren::dataclasses::SecondaryParticleRecord & other = secondaries[other_index]; + + + lepton.SetFourMomentum({p3.e(), p3.px(), p3.py(), p3.pz()}); + lepton.SetMass(p3.m()); + lepton.SetHelicity(record.primary_helicity); + + other.SetFourMomentum({p4.e(), p4.px(), p4.py(), p4.pz()}); + other.SetMass(p4.m()); + other.SetHelicity(record.target_helicity); +} + +double CharmDISFromSpline::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { + double dxs = DifferentialCrossSection(interaction); + double txs = TotalCrossSection(interaction); + if(dxs == 0) { + return 0.0; + } else { + return dxs / txs; + } +} + +std::vector CharmDISFromSpline::GetPossiblePrimaries() const { + return std::vector(primary_types_.begin(), primary_types_.end()); +} + +std::vector CharmDISFromSpline::GetPossibleTargetsFromPrimary(siren::dataclasses::ParticleType primary_type) const { + return std::vector(target_types_.begin(), target_types_.end()); +} + +std::vector CharmDISFromSpline::GetPossibleSignatures() const { + return std::vector(signatures_.begin(), signatures_.end()); +} + +std::vector CharmDISFromSpline::GetPossibleTargets() const { + return std::vector(target_types_.begin(), target_types_.end()); +} + +std::vector CharmDISFromSpline::GetPossibleSignaturesFromParents(siren::dataclasses::ParticleType primary_type, siren::dataclasses::ParticleType target_type) const { + std::pair key(primary_type, target_type); + if(signatures_by_parent_types_.find(key) != signatures_by_parent_types_.end()) { + return signatures_by_parent_types_.at(key); + } else { + return std::vector(); + } +} + +std::vector CharmDISFromSpline::DensityVariables() const { + return std::vector{"Bjorken x", "Bjorken y"}; +} + +} // namespace interactions +} // namespace siren diff --git a/projects/interactions/private/CharmHadronization.cxx b/projects/interactions/private/CharmHadronization.cxx new file mode 100644 index 000000000..e36ee5540 --- /dev/null +++ b/projects/interactions/private/CharmHadronization.cxx @@ -0,0 +1,166 @@ +#include "SIREN/interactions/CharmHadronization.h" + +#include // for array +#include // for sqrt, M_PI +#include // for basic_s... +#include // for vector +#include // for size_t + +#include // for Vector3 +#include // for P4, Boost + +#include "SIREN/interactions/Hadronization.h" // for Hadronization +#include "SIREN/dataclasses/InteractionRecord.h" // for Interac... +#include "SIREN/dataclasses/InteractionSignature.h" // for Interac... +#include "SIREN/dataclasses/Particle.h" // for Particle +#include "SIREN/utilities/Random.h" // for SIREN_random +#include "SIREN/utilities/Errors.h" // for PythonImplementationError +#include "SIREN/utilities/Constants.h" // for electronMass + + + +namespace siren { +namespace interactions { + +CharmHadronization::CharmHadronization() {} + +// pybind11::object CharmHadronization::get_self() { +// return pybind11::cast(Py_None); +// } + +bool CharmHadronization::equal(Hadronization const & other) const { + const CharmHadronization* x = dynamic_cast(&other); + + if(!x) + return false; + else + return primary_types == x->primary_types; +} + +double CharmHadronization::getHadronMass(siren::dataclasses::ParticleType hadron_type) { + switch(hadron_type){ + case siren::dataclasses::ParticleType::D0: + return( siren::utilities::Constants::D0Mass); + case siren::dataclasses::ParticleType::D0Bar: + return( siren::utilities::Constants::D0Mass); + case siren::dataclasses::ParticleType::DPlus: + return( siren::utilities::Constants::DPlusMass); + case siren::dataclasses::ParticleType::DMinus: + return( siren::utilities::Constants::DPlusMass); + case siren::dataclasses::ParticleType::Charm: + return( siren::utilities::Constants::CharmMass); + case siren::dataclasses::ParticleType::CharmBar: + return( siren::utilities::Constants::CharmMass); + default: + return(0.0); + } +} + + +std::vector CharmHadronization::GetPossibleSignatures() const +{ + std::vector signatures; + for(auto primary : primary_types) { + std::vector new_signatures = GetPossibleSignaturesFromParent(primary); + signatures.insert(signatures.end(),new_signatures.begin(),new_signatures.end()); + } + return signatures; +} + +std::vector CharmHadronization::GetPossibleSignaturesFromParent(siren::dataclasses::Particle::ParticleType primary) const { + std::vector signatures; + dataclasses::InteractionSignature signature; + signature.primary_type = primary; + signature.target_type = siren::dataclasses::Particle::ParticleType::Decay; + + signature.secondary_types.resize(2); + signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; + if(primary==siren::dataclasses::Particle::ParticleType::Charm) { + signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::D0; + signatures.push_back(signature); + signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::DPlus; + signatures.push_back(signature); + } + else if(primary==siren::dataclasses::Particle::ParticleType::CharmBar) { + signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::D0Bar; + signatures.push_back(signature); + signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::DMinus; + signatures.push_back(signature); + } + return signatures; +} + +void CharmHadronization::SampleFinalState(dataclasses::CrossSectionDistributionRecord & interaction, std::shared_ptr random) const { + // Uses Perterson distribution with epsilon = 0.2 + // charm quark momentum + rk::P4 pc(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); + double p3c = std::sqrt(std::pow(interaction.primary_momentum[1], 2) + std::pow(interaction.primary_momentum[2], 2) + std::pow(interaction.primary_momentum[3], 2)); + double Ec = pc.e(); //energy of primary charm + // std::cout << "hadronization sample final state" << std::endl; + // double peterson_distribution(double z) { + // return 0.8 / x * std::pow((1 - 1 / x - 0.2 / (1 - x)), 2) + // } + + double z = 0.6; // replace by actual sampling: inverse CDF + double ECH = z * Ec; + + // is it ok to compute everything in lab frame? + double mCH = getHadronMass(interaction.signature.secondary_types[1]); // obtain charmed hadron mass + double p3CH = std::sqrt(std::pow(ECH, 2) - std::pow(mCH, 2)); //obtain charmed hadron 3-momentum + double rCH = p3CH/p3c; // ratio of momentum carried away by the charmed hadron, assume collinearity + rk::P4 p4CH(geom3::Vector3(rCH * interaction.primary_momentum[1], rCH * interaction.primary_momentum[2], rCH * interaction.primary_momentum[3]), mCH); + + double EX = (1 - z) * Ec; // energy of the hadronic shower + double p3X = EX; // assume no hadronic mass + double rX = p3X/p3c; // assume collinear + rk::P4 p4X(geom3::Vector3(rX * interaction.primary_momentum[1], rX * interaction.primary_momentum[2], rX * interaction.primary_momentum[3]), 0); + + + // update interaction parameters: to be added here later + + // new implementation of updateing outgoing particles + std::vector & secondaries = interaction.GetSecondaryParticleRecords(); + siren::dataclasses::SecondaryParticleRecord & hadronic_vertex = secondaries[0]; + siren::dataclasses::SecondaryParticleRecord & d_meson = secondaries[1]; // these indices are hard-coded, should be automated in a future time + + hadronic_vertex.SetFourMomentum({p4X.e(), p4X.px(), p4X.py(), p4X.pz()}); + hadronic_vertex.SetMass(p4X.m()); + hadronic_vertex.SetHelicity(interaction.primary_helicity); + + d_meson.SetFourMomentum({p4CH.e(), p4CH.px(), p4CH.py(), p4CH.pz()}); + d_meson.SetMass(p4CH.m()); + d_meson.SetHelicity(interaction.primary_helicity); + + // interaction.secondary_momenta.resize(2); + // interaction.secondary_masses.resize(2); + // interaction.secondary_helicities.resize(2); + + // // the hadronic shower + // interaction.secondary_momenta[0][0] = p4X.e(); + // interaction.secondary_momenta[0][1] = p4X.px(); + // interaction.secondary_momenta[0][2] = p4X.py(); + // interaction.secondary_momenta[0][3] = p4X.pz(); + // interaction.secondary_masses[0] = 0; + // interaction.secondary_helicities[0] = interaction.primary_helicity; // true? + + // // the charmed hadron + // interaction.secondary_momenta[1][0] = p4CH.e(); + // interaction.secondary_momenta[1][1] = p4CH.px(); + // interaction.secondary_momenta[1][2] = p4CH.py(); + // interaction.secondary_momenta[1][3] = p4CH.pz(); + // interaction.secondary_masses[1] = p4CH.m(); + // interaction.secondary_helicities[1] = interaction.primary_helicity; // true? + +} + +double CharmHadronization::FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const { + if (secondary == siren::dataclasses::Particle::ParticleType::D0 || secondary == siren::dataclasses::Particle::ParticleType::D0Bar) { + return 0.6; + } else if (secondary == siren::dataclasses::Particle::ParticleType::DPlus || secondary == siren::dataclasses::Particle::ParticleType::DMinus) { + return 0.23; + } // D_s and Lambda^+ not yet implemented + return 0; +} + +} // namespace interactions +} // namespace siren \ No newline at end of file diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx new file mode 100644 index 000000000..7f6520abf --- /dev/null +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -0,0 +1,505 @@ +#include "SIREN/interactions/CharmMesonDecay.h" + +#include + +#include +#include + +#include "SIREN/dataclasses/InteractionRecord.h" // for Interac... +#include "SIREN/dataclasses/InteractionSignature.h" // for Interac... +#include "SIREN/dataclasses/Particle.h" // for Particle +#include "SIREN/math/Vector3D.h" // for Vector3D +#include "SIREN/utilities/Constants.h" // for GeV, pi +#include "SIREN/utilities/Random.h" // for SIREN_random + +#include "SIREN/utilities/Integration.h" // for rombergInt... +#include "SIREN/utilities/Interpolator.h" + +#include "SIREN/interactions/Decay.h" + +namespace siren { +namespace interactions { + +CharmMesonDecay::CharmMesonDecay() { + // this is the default initialization but should never be used + + // we need to compute cdf here b/c otherwise SampleFinalState becomes non constant + // in this case, we will need to compute all the possible dGamma's here + // maybe add a map here, but for now we hard code first + std::vector constants; + constants.resize(3); + // check the primary and secondaries of the signature + constants[0] = 0.725; // this is f^+(0)|V_cs| for charged D + constants[1] = 0.44; // this is alpha, same for all K final states + constants[2] = 2.01027; // this is excited charged D meson + + double mD = particleMass(siren::dataclasses::Particle::ParticleType::DPlus); + double mK = particleMass(siren::dataclasses::Particle::ParticleType::K0Bar); + + computeDiffGammaCDF(constants, mD, mK); +} + +CharmMesonDecay::CharmMesonDecay(siren::dataclasses::Particle::ParticleType primary) { + + //standard stuff, constant across primary types + std::vector constants; + constants.resize(3); + double mD; + double mK; + + if (primary == siren::dataclasses::Particle::ParticleType::DPlus) { + constants[0] = 0.725; // this is f^+(0)|V_cs| for charged D + constants[1] = 0.50; // this is alpha, same for all K final states + constants[2] = 2.01027; // this is excited charged D meson + + mD = particleMass(siren::dataclasses::Particle::ParticleType::DPlus); + mK = particleMass(siren::dataclasses::Particle::ParticleType::K0Bar); + + } else if (primary == siren::dataclasses::Particle::ParticleType::D0) { + constants[0] = 0.719; // this is f^+(0)|V_cs| for charged D + constants[1] = 0.50; // this is alpha, same for all K final states + constants[2] = 2.00697; // this is excited charged D meson + + mD = particleMass(siren::dataclasses::Particle::ParticleType::D0); + mK = particleMass(siren::dataclasses::Particle::ParticleType::KMinus); + } + + computeDiffGammaCDF(constants, mD, mK); + +} + +bool CharmMesonDecay::equal(Decay const & other) const { + const CharmMesonDecay* x = dynamic_cast(&other); + + if(!x) + return false; + else + return primary_types == x->primary_types; +} + + +double CharmMesonDecay::particleMass(siren::dataclasses::ParticleType particle) { + switch(particle){ + case siren::dataclasses::ParticleType::D0: + return( siren::utilities::Constants::D0Mass); + case siren::dataclasses::ParticleType::D0Bar: + return( siren::utilities::Constants::D0Mass); + case siren::dataclasses::ParticleType::DPlus: + return( siren::utilities::Constants::DPlusMass); + case siren::dataclasses::ParticleType::DMinus: + return( siren::utilities::Constants::DPlusMass); + case siren::dataclasses::ParticleType::K0: + return( siren::utilities::Constants::K0Mass); + case siren::dataclasses::ParticleType::K0Bar: + return( siren::utilities::Constants::K0Mass); + case siren::dataclasses::ParticleType::KPlus: + return( siren::utilities::Constants::KplusMass); + case siren::dataclasses::ParticleType::KMinus: + return( siren::utilities::Constants::KminusMass); + case siren::dataclasses::ParticleType::EPlus: + return( siren::utilities::Constants::electronMass ); + case siren::dataclasses::ParticleType::EMinus: + return( siren::utilities::Constants::electronMass ); + case siren::dataclasses::ParticleType::MuPlus: + return( siren::utilities::Constants::muonMass ); + case siren::dataclasses::ParticleType::MuMinus: + return( siren::utilities::Constants::muonMass ); + case siren::dataclasses::ParticleType::TauPlus: + return( siren::utilities::Constants::tauMass ); + case siren::dataclasses::ParticleType::TauMinus: + return( siren::utilities::Constants::tauMass ); + default: + return(0.0); + } +} + +double CharmMesonDecay::TotalDecayWidth(dataclasses::InteractionRecord const & record) const { + return TotalDecayWidth(record.signature.primary_type); +} + +// in this implementation, should we take total decay width to be only the channels we considered? +double CharmMesonDecay::TotalDecayWidth(siren::dataclasses::Particle::ParticleType primary) const { + double total_width = 0; + std::vector possible_signatures = GetPossibleSignaturesFromParent(primary); + for (auto sig : possible_signatures) { + // make a fake record and full from total decay width for final state + siren::dataclasses::InteractionRecord fake_record; + fake_record.signature = sig; + double this_width = TotalDecayWidthForFinalState(fake_record); + total_width += this_width; + } + return total_width; +} + +// current problem: in implementation we see kaons and pions both as hadrons, but they should have different branching ratios and form factors +double CharmMesonDecay::TotalDecayWidthForFinalState(dataclasses::InteractionRecord const & record) const { + double branching_ratio; + double tau; // total lifetime for all visible and invisible modes + // read in the signature and types + siren::dataclasses::Particle::ParticleType primary = record.signature.primary_type; + std::vector secondaries_vector = record.signature.secondary_types; + std::set secondaries = std::set(secondaries_vector.begin(), secondaries_vector.end()); + + // define the decay modes + std::set k0_eplus_nue = {siren::dataclasses::Particle::ParticleType::K0Bar, + siren::dataclasses::Particle::ParticleType::EPlus, + siren::dataclasses::Particle::ParticleType::NuE}; + std::set kminus_eplus_nue = {siren::dataclasses::Particle::ParticleType::KMinus, + siren::dataclasses::Particle::ParticleType::EPlus, + siren::dataclasses::Particle::ParticleType::NuE}; + if (primary == siren::dataclasses::Particle::ParticleType::DPlus && secondaries == k0_eplus_nue) { + // branching_ratio = 0.089; + branching_ratio = 1; + tau = 1040 * (1e-15); + } else if (primary == siren::dataclasses::Particle::ParticleType::D0 && secondaries == kminus_eplus_nue) { + // branching_ratio = 0.03538; + branching_ratio = 1; + tau = 410.1 * (1e-15); + } + else { + std::cout << "this decay mode is not yet implemented!" << std::endl; + } + return branching_ratio * siren::utilities::Constants::hbar / tau * siren::utilities::Constants::GeV; +} + + +std::vector CharmMesonDecay::GetPossibleSignatures() const { + std::vector signatures; + for(auto primary : primary_types) { + std::vector new_signatures = GetPossibleSignaturesFromParent(primary); + signatures.insert(signatures.end(),new_signatures.begin(),new_signatures.end()); + } + return signatures; +} + +std::vector CharmMesonDecay::GetPossibleSignaturesFromParent(siren::dataclasses::Particle::ParticleType primary) const { + std::vector signatures; + dataclasses::InteractionSignature signature; + signature.primary_type = primary; + signature.target_type = siren::dataclasses::Particle::ParticleType::Decay; + + // first we deal with semileptonic decays where there are 3 final state particles + signature.secondary_types.resize(3); + if(primary==siren::dataclasses::Particle::ParticleType::DPlus) { + signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::K0Bar; + signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::EPlus; + signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuE; + signatures.push_back(signature); + } else if (primary==siren::dataclasses::Particle::ParticleType::D0) { + signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::KMinus; + signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::EPlus; + signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuE; + signatures.push_back(signature); + } + else { + std::cout << "this D meson decay has not been implemented yet" << std::endl; + } + return signatures; +} + +std::vector CharmMesonDecay::FormFactorFromRecord(dataclasses::CrossSectionDistributionRecord const & record) const { + dataclasses::InteractionSignature signature = record.signature; + std::vector constants; + constants.resize(3); + // check the primary and secondaries of the signature + if (signature.primary_type == dataclasses::Particle::ParticleType::DPlus && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::K0Bar) { + constants[0] = 0.725; // this is f^+(0)|V_cs| for charged D + constants[1] = 0.44; // this is alpha, same for all K final states + constants[2] = 2.01027; // this is excited charged D meson + } else if (signature.primary_type == dataclasses::Particle::ParticleType::D0 && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::KMinus) { + constants[0] = 0.719; // this is f^+(0)|V_cs| for neutral D + constants[1] = 0.44; // this is alpha, same for all K final states + constants[2] = 2.00697; // this is excited neutral D meson + } + return constants; +} + +double CharmMesonDecay::DifferentialDecayWidth(dataclasses::InteractionRecord const & record) const { + // get the form factor constants + std::vector constants = FormFactorFromRecord(record); + // calculate the q^2 + rk::P4 pD(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); + rk::P4 pKPi(geom3::Vector3(record.secondary_momenta[0][1], record.secondary_momenta[0][2], record.secondary_momenta[0][3]), record.secondary_masses[0]); + double Q2 = (pD - pKPi).dot(pD - pKPi); + // primary and secondary masses are also needed + double mD = record.primary_mass; + double mK = record.secondary_masses[0]; + return DifferentialDecayWidth(constants, Q2, mD, mK); +} + +double CharmMesonDecay::DifferentialDecayWidth(std::vector constants, double Q2, double mD, double mK) const { + // get the numerical constants from the vector + double F0CKM = constants[0]; + double alpha = constants[1]; + double ms = constants[2]; + double Q2tilde = Q2 / ms; + // compute the 3-momentum as a function of Q2 + double EK = 0.5 * (Q2 - pow(mD, 2 + pow(mK, 2))) / mD; // energy of Kaon + double PK = pow(pow(EK, 2) - pow(mK, 2), 1/2); + // plug in the constants + double dGamma = pow(siren::utilities::Constants::FermiConstant,2) / (24 * pow(siren::utilities::Constants::pi,3)) * pow(F0CKM,2) * + pow((1/((1-Q2tilde) * (1 - alpha * Q2tilde))),2) * pow(PK,3); + return dGamma; +} + +void CharmMesonDecay::computeDiffGammaCDF(std::vector constants, double mD, double mK) { + + // returns a 1D interpolater table for dGamma cdf + // define the pdf with only Q2 as the input + std::function pdf = [&] (double x) -> double { + return DifferentialDecayWidth(constants, x, mD, mK); + }; + // first normalize the integral + double min = 0; + double max = 1.4; // these set the min and max of the Q2 considered + double normalization = siren::utilities::rombergIntegrate(pdf, min, max); + std::function normed_pdf = [&] (double x) -> double { + return DifferentialDecayWidth(constants, x, mD, mK) / normalization; + }; + // now create the spline and compute the CDF + + // set the Q2 nodes (use 100 nodes) + std::vector Q2spline; + for (int i = 0; i < 100; ++i) { + Q2spline.push_back(0.01 + i * (max-min) / 100 ); + } + + // declare the cdf vectors + std::vector cdf_vector; + std::vector cdf_Q2_nodes; + std::vector pdf_vector; + + cdf_Q2_nodes.push_back(0); + cdf_vector.push_back(0); + pdf_vector.push_back(0); + + // compute the spline table + for (int i = 0; i < Q2spline.size(); ++i) { + if (i == 0) { + double cur_Q2 = Q2spline[i]; + double cur_pdf = normed_pdf(cur_Q2); + double area = cur_Q2 * cur_pdf * 0.5; + pdf_vector.push_back(cur_pdf); + cdf_vector.push_back(area); + cdf_Q2_nodes.push_back(cur_Q2); + continue; + } + double cur_Q2 = Q2spline[i]; + double cur_pdf = normed_pdf(cur_Q2); + double area = 0.5 * (pdf_vector[i - 1] + cur_pdf) * (Q2spline[i] - Q2spline[i - 1]); + pdf_vector.push_back(cur_pdf); + cdf_Q2_nodes.push_back(cur_Q2); + cdf_vector.push_back(area + cdf_vector.back()); + } + + cdf_Q2_nodes.push_back(max); + cdf_vector.push_back(1); + pdf_vector.push_back(0); + + // for debugging and plotting, print the pdf and cdf tables + // for (size_t i = 0; auto& element : cdf_Q2_nodes) { + // std::cout << element; + // // Print comma if it's not the last element + // if (++i != cdf_Q2_nodes.size()) { + // std::cout << ", "; + // } + // } + // std::cout << std::endl; + + // for (size_t i = 0; auto& element : cdf_vector) { + // std::cout << element; + // // Print comma if it's not the last element + // if (++i != cdf_vector.size()) { + // std::cout << ", "; + // } + // } + // std::cout << std::endl; + + // for (size_t i = 0; auto& element : pdf_vector) { + // std::cout << element; + // // Print comma if it's not the last element + // if (++i != pdf_vector.size()) { + // std::cout << ", "; + // } + // } + // std::cout << std::endl; + + // set the spline table + siren::utilities::TableData1D inverse_cdf_data; + inverse_cdf_data.x = cdf_vector; + inverse_cdf_data.f = cdf_Q2_nodes; + + inverseCdf = siren::utilities::Interpolator1D(inverse_cdf_data); + + return; + +} + + + +void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { + // first obtain the constants needed for further computation from the signature + // std::cout<<"b1"< constants = FormFactorFromRecord(record); + double mD = particleMass(record.signature.primary_type); + double mK = particleMass(record.signature.secondary_types[0]); + + // std::cout << "input masses: " << mD << " " << mK << std::endl; + + // first sample a q^2 + //////////////////////////////////////////// + // computeDiffGammaCDF(constants, mD, mK);// + //////////////////////////////////////////// + double rand_value_for_Q2 = random->Uniform(0, 1); + double Q2 = inverseCdf(rand_value_for_Q2); + + // now sample isotropically the "zenith" direction + double cosTheta = random->Uniform(-1, 1); + double sinTheta = std::sin(std::acos(cosTheta)); + // set the x axis to be the D direction + geom3::UnitVector3 x_dir = geom3::UnitVector3::xAxis(); + // std::cout<<"b2"<Uniform(0, 2 * M_PI); + geom3::Rotation3 azimuth_rand_rot(p3D_lab_dir, phi); + p4K_Drest.rotate(azimuth_rand_rot); + p4W_Drest.rotate(azimuth_rand_rot); + // finally, boost the 4 momenta back to the lab frame + rk::Boost boost_from_Drest_to_lab = p4D_lab.labBoost(); + rk::P4 p4K_lab = p4K_Drest.boost(boost_from_Drest_to_lab); + rk::P4 p4W_lab = p4W_Drest.boost(boost_from_Drest_to_lab); + // std::cout<<"b4"<W+K/Pi decay, now treat the W->l+nu decay + double ml = particleMass(record.signature.secondary_types[1]); + double mnu = 0; + double W_cosTheta = random->Uniform(-1, 1); // sampling the direction + double W_sinTheta = std::sin(std::acos(W_cosTheta)); + double El = (Q2 + pow(ml, 2)) / (2 * sqrt(Q2)); + double Enu = (Q2 - pow(ml, 2)) / (2 * sqrt(Q2)); // the energies of the outgoing lepton and neutrino + double P = (Q2 - pow(ml, 2)) / (2 * sqrt(Q2)); + // now we have thr four vectors of the outgoing particle kinematics in tne W rest frame wrt x direction + rk::P4 p4l_Wrest(P * geom3::Vector3(W_cosTheta, W_sinTheta, 0), ml); + rk::P4 p4nu_Wrest(P * geom3::Vector3(-W_cosTheta, -W_sinTheta, 0), 0); + + // std::cout << "momentums: " << p4l_Wrest << " " << p4nu_Wrest << std::endl; + // std::cout << "check mass of l and nu: " << p4l_Wrest.m() << " " << p4nu_Wrest.m() << std::endl; + //now rotate so they are defined wrt the lab frame W direction + // std::cout<<"b5"<Uniform(0, 2 * M_PI); + geom3::Rotation3 W_azimuth_rand_rot(p3W_lab_dir, W_phi); + rk::P4 p4l_lab = p4l_Wrest.rotate(W_azimuth_rand_rot); + rk::P4 p4nu_lab = p4nu_Wrest.rotate(W_azimuth_rand_rot); + + // std::cout<<"b6"< & secondaries = record.GetSecondaryParticleRecords(); + siren::dataclasses::SecondaryParticleRecord & kpi = secondaries[0]; + siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[1]; + siren::dataclasses::SecondaryParticleRecord & neutrino = secondaries[2]; //these are all hardcoded at the time + + kpi.SetFourMomentum({p4K_lab.e(), p4K_lab.px(), p4K_lab.py(), p4K_lab.pz()}); + kpi.SetMass(p4K_lab.m()); + kpi.SetHelicity(record.primary_helicity); + + lepton.SetFourMomentum({p4l_lab.e(), p4l_lab.px(), p4l_lab.py(), p4l_lab.pz()}); + lepton.SetMass(p4l_lab.m()); + lepton.SetHelicity(record.primary_helicity); + + neutrino.SetFourMomentum({p4nu_lab.e(), p4nu_lab.px(), p4nu_lab.py(), p4nu_lab.pz()}); + neutrino.SetMass(p4nu_lab.m()); + neutrino.SetHelicity(record.primary_helicity); + + // finally, we can populate the record, for implementation in prometheus, maybe add treatment of hadrons, but could be implemnted on p side + // record.secondary_momenta.resize(3); + // record.secondary_masses.resize(3); + // record.secondary_helicity.resize(3); // 0 is the hadron, 1 is the lepton, 2 is the neutrino + // // the K/pi + // record.secondary_momenta[0][0] = p4K_lab.e(); + // record.secondary_momenta[0][1] = p4K_lab.px(); + // record.secondary_momenta[0][2] = p4K_lab.py(); + // record.secondary_momenta[0][3] = p4K_lab.pz(); + // record.secondary_masses[0] = p4K_lab.m(); + // record.secondary_helicity[0] = 0; + // // the lepton + // record.secondary_momenta[1][0] = p4l_lab.e(); + // record.secondary_momenta[1][1] = p4l_lab.px(); + // record.secondary_momenta[1][2] = p4l_lab.py(); + // record.secondary_momenta[1][3] = p4l_lab.pz(); + // record.secondary_masses[1] = p4l_lab.m(); + // record.secondary_helicity[1] = 1; + // // the neutrino + // record.secondary_momenta[2][0] = p4nu_lab.e(); + // record.secondary_momenta[2][1] = p4nu_lab.px(); + // record.secondary_momenta[2][2] = p4nu_lab.py(); + // record.secondary_momenta[2][3] = p4nu_lab.pz(); + // record.secondary_masses[2] = p4nu_lab.m(); + // record.secondary_helicity[2] = 1; + + //for debug purposes + // double p4w_rest_Q2 = pow(p4W_Drest.e(), 2) - pow(p4W_Drest.px(), 2) - + // pow(p4W_Drest.py(), 2) - pow(p4W_Drest.pz(), 2); + // double p4w_lab_Q2 = pow(p4W_lab.e(), 2) - pow(p4W_lab.px(), 2) - + // pow(p4W_lab.py(), 2) - pow(p4W_lab.pz(), 2); + // std::cout << p4W_Drest.e() << " " << p4W_lab.e() << " " << PW << " " << p4W_Drest.p() << " " << p4W_Drest << std::endl; + // std::cout << p4K_Drest.e() << " " << p4K_lab.e() << " " << PK << " " << p4K_Drest.p() << " " << p4K_Drest << std::endl; + // std::cout << Q2 << " " << sqrt(Q2)<< std::endl; + // std::cout << "invariant mass of the W in two frames are " << p4w_lab_Q2 << " " << p4w_rest_Q2 << std::endl; + // std::cout << "check mass of W: " << p4W_lab.m() << " " << p4W_Drest.m() << std::endl; + // std::cout << "check mass of K: " << p4K_lab.m() << " " << p4K_Drest.m() << std::endl; + + + + // rk::P4 inv_mass_Wrest = p4l_Wrest + p4nu_Wrest; + // rk::P4 inv_mass_lab = p4l_lab + p4nu_lab; + // std::cout << "inv masses in two frames: " << pow(inv_mass_Wrest.m(), 2) << " " << pow(inv_mass_lab.m(), 2) << std::endl; + // std::cout << "using inv mass calculator: " << pow(invMass(p4l_Wrest, p4nu_Wrest), 2) << " " << pow(invMass(p4l_lab, p4nu_lab), 2) << std::endl; + // std::cout << "energy of l and nu and inv mass: " << El << " " << Enu << " " << pow(El+Enu, 2) << std::endl; + // std::cout << "momentums: " << p4l_Wrest << " " << p4nu_Wrest << std::endl; + // std::cout << "check mass of l and nu: " << p4l_Wrest.m() << " " << p4nu_Wrest.m() << std::endl; + // std::cout << "W energy in rest frame: " << pow(p4W_lab.m(), 2) << std::endl; + + +} + +double CharmMesonDecay::FinalStateProbability(dataclasses::InteractionRecord const & record) const { + double dd = DifferentialDecayWidth(record); + double td = TotalDecayWidthForFinalState(record); + if (dd == 0) return 0.; + else if (td == 0) return 0.; + else return dd/td; +} + +std::vector CharmMesonDecay::DensityVariables() const { + return std::vector{"Q2"}; +} + + + +} // namespace interactions +} // namespace siren + diff --git a/projects/interactions/private/DMesonELoss.cxx b/projects/interactions/private/DMesonELoss.cxx new file mode 100644 index 000000000..867f7edf2 --- /dev/null +++ b/projects/interactions/private/DMesonELoss.cxx @@ -0,0 +1,281 @@ +#include "SIREN/interactions/DMesonELoss.h" + +#include // for map, opera... +#include // for set, opera... +#include // for array +#include // for pow, log10 +#include // for tie, opera... +#include // for allocator +#include // for basic_string +#include // for vector +#include // for assert +#include // for size_t + +#include // for P4, Boost +#include // for Vector3 + +#include // for splinetable +//#include + +#include "SIREN/interactions/CrossSection.h" // for CrossSection +#include "SIREN/dataclasses/InteractionRecord.h" // for Interactio... +#include "SIREN/dataclasses/Particle.h" // for Particle +#include "SIREN/utilities/Random.h" // for SIREN_random +#include "SIREN/utilities/Constants.h" // for electronMass +#include "SIREN/utilities/Integration.h" // for rombergInt... + + +namespace siren { +namespace interactions { + +DMesonELoss::DMesonELoss() { +} + + +bool DMesonELoss::equal(CrossSection const & other) const { + const DMesonELoss* x = dynamic_cast(&other); + + if(!x) + return false; + else + return + std::tie( + primary_types_, + target_types_) + == + std::tie( + x->primary_types_, + x->target_types_); +} + + + +std::vector DMesonELoss::GetPossiblePrimaries() const { + return std::vector(primary_types_.begin(), primary_types_.end()); +} + +// getting target should be the same for all the primary types +std::vector DMesonELoss::GetPossibleTargetsFromPrimary(siren::dataclasses::Particle::ParticleType primary_type) const { + return std::vector(target_types_.begin(), target_types_.end()); +} + +std::vector DMesonELoss::GetPossibleTargets() const { + return std::vector(target_types_.begin(), target_types_.end()); +} + +std::vector DMesonELoss::GetPossibleSignatures() const { + std::vector signatures; + for(auto primary : primary_types_) { + // hardcode the target type here, this should be fine + std::vector new_signatures = GetPossibleSignaturesFromParents(primary, siren::dataclasses::Particle::ParticleType::PPlus); + signatures.insert(signatures.end(),new_signatures.begin(),new_signatures.end()); + } + return signatures; +} + +std::vector DMesonELoss::GetPossibleSignaturesFromParents(siren::dataclasses::Particle::ParticleType primary_type, siren::dataclasses::Particle::ParticleType target_type) const { + std::vector signatures; + dataclasses::InteractionSignature signature; + signature.primary_type = primary_type; + signature.target_type = target_type; + + // first we deal with semileptonic decays where there are 3 final state particles + signature.secondary_types.resize(1); + signature.secondary_types[0] = primary_type; // same particle comes out + signatures.push_back(signature); + return signatures; +} + +// i am here + +double DMesonELoss::TotalCrossSection(dataclasses::InteractionRecord const & interaction) const { + siren::dataclasses::Particle::ParticleType primary_type = interaction.signature.primary_type; + rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); + // rk::P4 p2(geom3::Vector3(interaction.target_momentum[1], interaction.target_momentum[2], interaction.target_momentum[3]), interaction.target_mass); + // double primary_energy; + + // // make sure of the reference frame before assigning energy + // if(interaction.target_momentum[1] == 0 and interaction.target_momentum[2] == 0 and interaction.target_momentum[3] == 0) { + // primary_energy = interaction.primary_momentum[0]; + // } else { + // rk::Boost boost_start_to_lab = p2.restBoost(); + // rk::P4 p1_lab = boost_start_to_lab * p1; + // primary_energy = p1_lab.e(); + // } + double primary_energy = interaction.primary_momentum[0]; + + + return TotalCrossSection(primary_type, primary_energy); +} + +double DMesonELoss::TotalCrossSection(siren::dataclasses::Particle::ParticleType primary_type, double primary_energy) const { + if(not primary_types_.count(primary_type)) { + throw std::runtime_error("Supplied primary not supported by cross section!"); + } + double log_energy = log10(primary_energy); + double mb_to_cm2 = 1e-27; + + // current implementation uses only > 1PeV data + double xsec = exp(1.891 + 0.205 * log_energy) - 2.157 + 1.264 * log_energy; + + return xsec * mb_to_cm2; +} + +// double DMesonELoss::TotalCrossSection(siren::dataclasses::Particle::ParticleType primary_type, double primary_energy, siren::dataclasses::Particle::ParticleType target) const { +// return DMesonELoss::TotalCrossSection(primary_type,primary_energy); +// } + + +double DMesonELoss::DifferentialCrossSection(dataclasses::InteractionRecord const & interaction) const { + rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); + // rk::P4 p2(geom3::Vector3(interaction.target_momentum[1], interaction.target_momentum[2], interaction.target_momentum[3]), interaction.target_mass); + double primary_energy; + rk::P4 p1_lab; + // rk::P4 p2_lab; + // if(interaction.target_momentum[1] == 0 and interaction.target_momentum[2] == 0 and interaction.target_momentum[3] == 0) { + primary_energy = interaction.primary_momentum[0]; + p1_lab = p1; + // p2_lab = p2; + // } else { + // rk::Boost boost_start_to_lab = p2.restBoost(); + // p1_lab = boost_start_to_lab * p1; + // p2_lab = boost_start_to_lab * p2; + // primary_energy = p1_lab.e(); + // std::cout << "D Meson Diff Xsec: not in lab frame???" << std::endl; + // } + + double final_energy = interaction.secondary_momenta[0][0]; + double z = 1 - final_energy / primary_energy; + + // now normalize the gaussian + double total_xsec = TotalCrossSection(interaction.signature.primary_type, primary_energy); + double z0 = 0.56; + double sigma = 0.2; + std::function integrand = [&] (double z) -> double { + return exp(-(pow(z - z0, 2))/(2 * pow(sigma, 2))); + }; + double unnormalized = siren::utilities::rombergIntegrate(integrand, 0.001, 0.999); + double normalization = total_xsec / unnormalized; + + double diff_xsec = normalization * exp(-(pow(z - z0, 2))/(2 * pow(sigma, 2))); + + return diff_xsec; +} + + +double DMesonELoss::InteractionThreshold(dataclasses::InteractionRecord const & interaction) const { + // Consider implementing DIS thershold at some point + return 0; +} + +void DMesonELoss::SampleFinalState(dataclasses::CrossSectionDistributionRecord& interaction, std::shared_ptr random) const { + + // std::cout << "In D Meson E Loss Sample Final State" << std::endl; + + rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); + // rk::P4 p2(geom3::Vector3(interaction.target_momentum[1], interaction.target_momentum[2], interaction.target_momentum[3]), interaction.target_mass); + + // we assume that: + // the target is stationary so its energy is just its mass + // the incoming neutrino is massless, so its kinetic energy is its total energy + // double s = target_mass_ * tinteraction.secondary_momentarget_mass_ + 2 * target_mass_ * primary_energy; + // double s = std::pow(rk::invMass(p1, p2), 2); + + double primary_energy; + double Dmass = interaction.primary_mass; + rk::P4 p1_lab; + // rk::P4 p2_lab; + // if(interaction.target_momentum[1] == 0 and interaction.target_momentum[2] == 0 and interaction.target_momentum[3] == 0) { + p1_lab = p1; + // p2_lab = p2; + primary_energy = p1_lab.e(); + // } else { + // // this is currently not implemented + // // Rest frame of p2 will be our "lab" frame + // rk::Boost boost_start_to_lab = p2.restBoost(); + // p1_lab = boost_start_to_lab * p1; + // p2_lab = boost_start_to_lab * p2; + // primary_energy = p1_lab.e(); + // // std::cout << "D Meson Energy Loss: not in lab frame???" << std::endl; + // } + // following line is wrong but i dont want to change it now fuck it. + // std::cout << " " << interaction.primary_momentum[0] << " " << interaction.primary_momentum[1] << " " << interaction.primary_momentum[2] << " " << interaction.primary_momentum[3]; + // std::cout << primary_energy << " " << pow(primary_energy, 2) - pow(Dmass, 2) << " " << + // sqrt(pow(interaction.primary_momentum[1], 2) +pow(interaction.primary_momentum[2], 2) +pow(interaction.primary_momentum[3], 2)) << std::endl; + // sample an inelasticity from gaussian using Box-Muller Transform + double sigma = 0.2; + double z0 = 0.56; // for mesons only, for baryons it's 0.59, but not implemented yet + double u1, u2; + double final_energy; + bool accept; + + + do { + do + { + u1 = random->Uniform(0, 1); + } + while (u1 == 0); + u2 = random->Uniform(0, 1); + double z = sigma * sqrt(-2.0 * log(u1)) * cos(2.0 * M_PI * u2) + z0; + // std::cout << z<< std::endl; + + // now modify the energy of the charm hadron and the corresponding momentum + final_energy = primary_energy * (1-z); + if (pow(final_energy, 2) - pow(Dmass, 2) >= 0) { + accept = true; + } else { + accept = false; + } + } while (!accept); + // this might be an infinite loop??????? + // need to check if the cross section length is good enough, how to make some relevant plots? + // std::cout << final_energy << std::endl; + double p3f = sqrt(pow(final_energy, 2) - pow(Dmass, 2)); + double p3i = std::sqrt(std::pow(p1.px(), 2) + std::pow(p1.py(), 2) + std::pow(p1.pz(), 2)); + double p_ratio = p3f / p3i; + + // std::cout << " " << p3f << " " << p3i << " " << p_ratio << std::endl; + rk::P4 pf(p_ratio * geom3::Vector3(p1.px(), p1.py(), p1.pz()), Dmass); + + std::vector & secondaries = interaction.GetSecondaryParticleRecords(); + siren::dataclasses::SecondaryParticleRecord & dmeson = secondaries[0]; + + + dmeson.SetFourMomentum({pf.e(), pf.px(), pf.py(), pf.pz()}); + dmeson.SetMass(pf.m()); + dmeson.SetHelicity(interaction.primary_helicity); + + // interaction.secondary_momenta.resize(1); + // interaction.secondary_masses.resize(1); + // interaction.secondary_helicity.resize(1); + + // interaction.secondary_momenta[0][0] = pf.e(); // p3_energy + // interaction.secondary_momenta[0][1] = pf.px(); // p3_x + // interaction.secondary_momenta[0][2] = pf.py(); // p3_y + // interaction.secondary_momenta[0][3] = pf.pz(); // p3_z + // interaction.secondary_masses[0] = pf.m(); + + // interaction.secondary_helicity[0] = interaction.primary_helicity; +} + +double DMesonELoss::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { + double dxs = DifferentialCrossSection(interaction); + double txs = TotalCrossSection(interaction); + if(dxs == 0) { + return 0.0; + } else { + return dxs / txs; + } +} + +std::vector DMesonELoss::DensityVariables() const { + return std::vector{"Bjorken y"}; +} + +void DMesonELoss::InitializeSignatures() { + return; +} + +} // namespace interactions +} // namespace siren diff --git a/projects/interactions/private/Hadronization.cxx b/projects/interactions/private/Hadronization.cxx new file mode 100644 index 000000000..7833c0735 --- /dev/null +++ b/projects/interactions/private/Hadronization.cxx @@ -0,0 +1,17 @@ +#include "SIREN/interactions/Hadronization.h" +#include "SIREN/dataclasses/InteractionRecord.h" + +namespace siren { +namespace interactions { + +Hadronization::Hadronization() {} + +bool Hadronization::operator==(Hadronization const & other) const { + if(this == &other) + return true; + else + return this->equal(other); +} + +} // namespace interactions +} // namespace siren \ No newline at end of file diff --git a/projects/interactions/private/InteractionCollection.cxx b/projects/interactions/private/InteractionCollection.cxx index 0f68772ba..e4a3a87d0 100644 --- a/projects/interactions/private/InteractionCollection.cxx +++ b/projects/interactions/private/InteractionCollection.cxx @@ -8,7 +8,8 @@ #include // for pair #include "SIREN/interactions/CrossSection.h" // for CrossSe... -#include "SIREN/interactions/Decay.h" // for Decay +#include "SIREN/interactions/Decay.h" // for Decay +#include "SIREN/interactions/Hadronization.h" // for Decay #include "SIREN/dataclasses/InteractionRecord.h" // for Interac... #include "SIREN/dataclasses/InteractionSignature.h" // for Interac... #include "SIREN/dataclasses/Particle.h" // for Particle @@ -60,6 +61,22 @@ InteractionCollection::InteractionCollection(siren::dataclasses::ParticleType pr InitializeTargetTypes(); } +InteractionCollection::InteractionCollection(siren::dataclasses::Particle::ParticleType primary_type, std::vector> hadronizations) : primary_type(primary_type), hadronizations(hadronizations) { + InitializeTargetTypes(); +} + +InteractionCollection::InteractionCollection(siren::dataclasses::Particle::ParticleType primary_type, std::vector> cross_sections, std::vector> hadronizations) : primary_type(primary_type), cross_sections(cross_sections), hadronizations(hadronizations) { + InitializeTargetTypes(); +} + +InteractionCollection::InteractionCollection(siren::dataclasses::Particle::ParticleType primary_type, std::vector> decays, std::vector> hadronizations) : primary_type(primary_type), decays(decays), hadronizations(hadronizations) { + InitializeTargetTypes(); +} + +InteractionCollection::InteractionCollection(siren::dataclasses::Particle::ParticleType primary_type, std::vector> cross_sections, std::vector> decays, std::vector> hadronizations) : primary_type(primary_type), cross_sections(cross_sections), decays(decays), hadronizations(hadronizations) { + InitializeTargetTypes(); +} + bool InteractionCollection::operator==(InteractionCollection const & other) const { return std::tie(primary_type, target_types, cross_sections, decays) diff --git a/projects/interactions/private/pybindings/CharmDISFromSpline.h b/projects/interactions/private/pybindings/CharmDISFromSpline.h new file mode 100644 index 000000000..486556be4 --- /dev/null +++ b/projects/interactions/private/pybindings/CharmDISFromSpline.h @@ -0,0 +1,87 @@ +#include +#include +#include + +#include +#include +#include + +#include "../../public/SIREN/interactions/CrossSection.h" +#include "../../public/SIREN/interactions/CharmDISFromSpline.h" +#include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" +#include "../../../dataclasses/public/SIREN/dataclasses/InteractionRecord.h" +#include "../../../dataclasses/public/SIREN/dataclasses/InteractionSignature.h" +#include "../../../geometry/public/SIREN/geometry/Geometry.h" +#include "../../../utilities/public/SIREN/utilities/Random.h" + +void register_CharmDISFromSpline(pybind11::module_ & m) { + using namespace pybind11; + using namespace siren::interactions; + + class_, CrossSection> charmdisfromspline(m, "CharmDISFromSpline"); + + charmdisfromspline + + .def(init<>()) + .def(init, std::vector, int, double, double, std::set, std::set, std::string>(), + arg("total_xs_data"), + arg("differential_xs_data"), + arg("interaction"), + arg("target_mass"), + arg("minimum_Q2"), + arg("primary_types"), + arg("target_types"), + arg("units") = std::string("cm")) + .def(init, std::vector, int, double, double, std::vector, std::vector, std::string>(), + arg("total_xs_data"), + arg("differential_xs_data"), + arg("interaction"), + arg("target_mass"), + arg("minimum_Q2"), + arg("primary_types"), + arg("target_types"), + arg("units") = std::string("cm")) + .def(init, std::set, std::string>(), + arg("total_xs_filename"), + arg("differential_xs_filename"), + arg("interaction"), + arg("target_mass"), + arg("minimum_Q2"), + arg("primary_types"), + arg("target_types"), + arg("units") = std::string("cm")) + .def(init, std::set, std::string>(), + arg("total_xs_filename"), + arg("differential_xs_filename"), + arg("primary_types"), + arg("target_types"), + arg("units") = std::string("cm")) + .def(init, std::vector, std::string>(), + arg("total_xs_filename"), + arg("differential_xs_filename"), + arg("interaction"), + arg("target_mass"), + arg("minimum_Q2"), + arg("primary_types"), + arg("target_types"), + arg("units") = std::string("cm")) + .def(init, std::vector, std::string>(), + arg("total_xs_filename"), + arg("differential_xs_filename"), + arg("primary_types"), + arg("target_types"), + arg("units") = std::string("cm")) + .def(self == self) + .def("TotalCrossSection",overload_cast(&CharmDISFromSpline::TotalCrossSection, const_)) + .def("TotalCrossSection",overload_cast(&CharmDISFromSpline::TotalCrossSection, const_)) + .def("DifferentialCrossSection",overload_cast(&CharmDISFromSpline::DifferentialCrossSection, const_)) + .def("DifferentialCrossSection",overload_cast(&CharmDISFromSpline::DifferentialCrossSection, const_)) + .def("InteractionThreshold",&CharmDISFromSpline::InteractionThreshold) + .def("GetPossibleTargets",&CharmDISFromSpline::GetPossibleTargets) + .def("GetPossibleTargetsFromPrimary",&CharmDISFromSpline::GetPossibleTargetsFromPrimary) + .def("GetPossiblePrimaries",&CharmDISFromSpline::GetPossiblePrimaries) + .def("GetPossibleSignatures",&CharmDISFromSpline::GetPossibleSignatures) + .def("GetPossibleSignaturesFromParents",&CharmDISFromSpline::GetPossibleSignaturesFromParents) + .def("FinalStateProbability",&CharmDISFromSpline::FinalStateProbability); +} + diff --git a/projects/interactions/private/pybindings/CharmHadronization.h b/projects/interactions/private/pybindings/CharmHadronization.h new file mode 100644 index 000000000..c0c7c7788 --- /dev/null +++ b/projects/interactions/private/pybindings/CharmHadronization.h @@ -0,0 +1,38 @@ +#include +#include +#include + +#include +#include +#include + +#include "../../public/SIREN/interactions/Hadronization.h" +#include "../../public/SIREN/interactions/CharmHadronization.h" + + +#include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" +#include "../../../dataclasses/public/SIREN/dataclasses/InteractionRecord.h" +#include "../../../dataclasses/public/SIREN/dataclasses/InteractionSignature.h" +#include "../../../geometry/public/SIREN/geometry/Geometry.h" +#include "../../../utilities/public/SIREN/utilities/Random.h" + +using namespace pybind11; +using namespace siren::interactions; + + +void register_CharmHadronization(pybind11::module_ & m) { + using namespace pybind11; + using namespace siren::interactions; + + class_, Hadronization> charmhadronization(m, "CharmHadronization"); + + charmhadronization + + .def(init<>()) + .def(self == self) + .def("SampleFinalState",&CharmHadronization::SampleFinalState) + .def("GetPossibleSignatures",&CharmHadronization::GetPossibleSignatures) + .def("GetPossibleSignaturesFromParent",&CharmHadronization::GetPossibleSignaturesFromParent) + .def("FragmentationFraction",&CharmHadronization::FragmentationFraction); + ; +} diff --git a/projects/interactions/private/pybindings/CharmMesonDecay.h b/projects/interactions/private/pybindings/CharmMesonDecay.h new file mode 100644 index 000000000..224ef9ad2 --- /dev/null +++ b/projects/interactions/private/pybindings/CharmMesonDecay.h @@ -0,0 +1,34 @@ +#include +#include +#include + +#include +#include +#include + +#include "../../public/SIREN/interactions/CrossSection.h" +#include "../../public/SIREN/interactions/Decay.h" +#include "../../public/SIREN/interactions/CharmMesonDecay.h" + +#include "../../public/SIREN/interactions/pyDecay.h" +#include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" +#include "../../../geometry/public/SIREN/geometry/Geometry.h" +#include "../../../utilities/public/SIREN/utilities/Random.h" + +void register_CharmMesonDecay(pybind11::module_ & m) { + using namespace pybind11; + using namespace siren::interactions; + + class_, Decay> charmmesondecay(m, "CharmMesonDecay"); + + charmmesondecay + + .def(init<>()) + .def(init(), + arg("primary_type")) + .def(self == self) + .def("SampleFinalState",&CharmMesonDecay::SampleFinalState) + .def("GetPossibleSignatures",&CharmMesonDecay::GetPossibleSignatures) + .def("GetPossibleSignaturesFromParent",&CharmMesonDecay::GetPossibleSignaturesFromParent); + +} diff --git a/projects/interactions/private/pybindings/DMesonELoss.h b/projects/interactions/private/pybindings/DMesonELoss.h new file mode 100644 index 000000000..427633c4e --- /dev/null +++ b/projects/interactions/private/pybindings/DMesonELoss.h @@ -0,0 +1,37 @@ +#include +#include +#include + +#include +#include +#include + +#include "../../public/SIREN/interactions/CrossSection.h" +#include "../../public/SIREN/interactions/CharmDISFromSpline.h" +#include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" +#include "../../../dataclasses/public/SIREN/dataclasses/InteractionRecord.h" +#include "../../../dataclasses/public/SIREN/dataclasses/InteractionSignature.h" +#include "../../../geometry/public/SIREN/geometry/Geometry.h" +#include "../../../utilities/public/SIREN/utilities/Random.h" + +void register_DMesonELoss(pybind11::module_ & m) { + using namespace pybind11; + using namespace siren::interactions; + + class_, CrossSection> dmesoneloss(m, "DMesonELoss"); + + dmesoneloss + .def(init<>()) + .def(self == self) + .def("TotalCrossSection",overload_cast(&DMesonELoss::TotalCrossSection, const_)) + .def("TotalCrossSection",overload_cast(&DMesonELoss::TotalCrossSection, const_)) + // .def("DifferentialCrossSection",overload_cast(&DMesonELoss::DifferentialCrossSection, const_)) + .def("InteractionThreshold",&DMesonELoss::InteractionThreshold) + .def("GetPossibleTargets",&DMesonELoss::GetPossibleTargets) + .def("GetPossibleTargetsFromPrimary",&DMesonELoss::GetPossibleTargetsFromPrimary) + .def("GetPossiblePrimaries",&DMesonELoss::GetPossiblePrimaries) + .def("GetPossibleSignatures",&DMesonELoss::GetPossibleSignatures) + .def("GetPossibleSignaturesFromParents",&DMesonELoss::GetPossibleSignaturesFromParents) + .def("FinalStateProbability",&DMesonELoss::FinalStateProbability); +} + diff --git a/projects/interactions/private/pybindings/Hadronization.h b/projects/interactions/private/pybindings/Hadronization.h new file mode 100644 index 000000000..513836cfe --- /dev/null +++ b/projects/interactions/private/pybindings/Hadronization.h @@ -0,0 +1,34 @@ +#include +#include +#include + +#include +#include +#include + +#include "../../public/SIREN/interactions/Hadronization.h" + +#include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" +#include "../../../dataclasses/public/SIREN/dataclasses/InteractionRecord.h" +#include "../../../dataclasses/public/SIREN/dataclasses/InteractionSignature.h" +#include "../../../geometry/public/SIREN/geometry/Geometry.h" +#include "../../../utilities/public/SIREN/utilities/Random.h" + +using namespace pybind11; +using namespace siren::interactions; + + +void register_Hadronization(pybind11::module_ & m) { + using namespace pybind11; + using namespace siren::interactions; + + class_>(m, "Hadronization") + // .def(init<>()) + .def("__eq__", [](const Hadronization &self, const Hadronization &other){ return self == other; }) + .def("equal", &Hadronization::equal) + .def("SampleFinalState", &Hadronization::SampleFinalState) + .def("GetPossibleSignatures", &Hadronization::GetPossibleSignatures) + .def("GetPossibleSignaturesFromParent", &Hadronization::GetPossibleSignaturesFromParent) + .def("FragmentationFraction", &Hadronization::FragmentationFraction) + ; +} diff --git a/projects/interactions/private/pybindings/InteractionCollection.h b/projects/interactions/private/pybindings/InteractionCollection.h index 5d8314acc..7eaf5b2c0 100644 --- a/projects/interactions/private/pybindings/InteractionCollection.h +++ b/projects/interactions/private/pybindings/InteractionCollection.h @@ -9,6 +9,8 @@ #include "../../public/SIREN/interactions/CrossSection.h" #include "../../public/SIREN/interactions/InteractionCollection.h" #include "../../public/SIREN/interactions/Decay.h" +#include "../../public/SIREN/interactions/Hadronization.h" + #include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" #include "../../../geometry/public/SIREN/geometry/Geometry.h" #include "../../../utilities/public/SIREN/utilities/Random.h" @@ -21,12 +23,17 @@ void register_InteractionCollection(pybind11::module_ & m) { .def(init<>()) .def(init>>()) .def(init>>()) + .def(init>>()) .def(init>, std::vector>>()) + .def(init>, std::vector>>()) + .def(init>, std::vector>>()) + .def(init>, std::vector>, std::vector>>()) .def(self == self) .def("GetCrossSections",&InteractionCollection::GetCrossSections, return_value_policy::reference_internal) .def("GetDecays",&InteractionCollection::GetDecays, return_value_policy::reference_internal) .def("HasCrossSections",&InteractionCollection::HasCrossSections) .def("HasDecays",&InteractionCollection::HasDecays) + .def("HasHadronizations",&InteractionCollection::HasHadronizations) .def("GetCrossSectionsForTarget",&InteractionCollection::GetCrossSectionsForTarget, return_value_policy::reference_internal) .def("GetCrossSectionsByTarget",&InteractionCollection::GetCrossSectionsByTarget, return_value_policy::reference_internal) .def("TotalCrossSectionByTarget",&InteractionCollection::TotalCrossSectionByTarget) diff --git a/projects/interactions/private/pybindings/interactions.cxx b/projects/interactions/private/pybindings/interactions.cxx index e030ac759..7cfc91cda 100644 --- a/projects/interactions/private/pybindings/interactions.cxx +++ b/projects/interactions/private/pybindings/interactions.cxx @@ -5,21 +5,31 @@ #include "../../public/SIREN/interactions/NeutrissimoDecay.h" #include "../../public/SIREN/interactions/InteractionCollection.h" #include "../../public/SIREN/interactions/DISFromSpline.h" +#include "../../public/SIREN/interactions/CharmDISFromSpline.h" #include "../../public/SIREN/interactions/HNLFromSpline.h" #include "../../public/SIREN/interactions/DipoleFromTable.h" #include "../../public/SIREN/interactions/DarkNewsCrossSection.h" #include "../../public/SIREN/interactions/DarkNewsDecay.h" +#include "../../public/SIREN/interactions/Hadronization.h" +#include "../../public/SIREN/interactions/CharmHadronization.h" +#include "../../public/SIREN/interactions/CharmMesonDecay.h" +#include "../../public/SIREN/interactions/DMesonELoss.h" #include "./CrossSection.h" #include "./DipoleFromTable.h" #include "./DarkNewsCrossSection.h" #include "./DarkNewsDecay.h" #include "./DISFromSpline.h" +#include "./CharmDISFromSpline.h" #include "./HNLFromSpline.h" #include "./Decay.h" #include "./NeutrissimoDecay.h" #include "./InteractionCollection.h" #include "./DummyCrossSection.h" +#include "./Hadronization.h" +#include "./CharmHadronization.h" +#include "./CharmMesonDecay.h" +#include "./DMesonELoss.h" #include #include @@ -34,10 +44,17 @@ PYBIND11_MODULE(interactions,m) { register_CrossSection(m); register_Decay(m); + register_Hadronization(m); + + register_CharmHadronization(m); + register_CharmMesonDecay(m); + register_DMesonELoss(m); + register_DipoleFromTable(m); register_DarkNewsCrossSection(m); register_DarkNewsDecay(m); register_DISFromSpline(m); + register_CharmDISFromSpline(m); register_HNLFromSpline(m); register_NeutrissimoDecay(m); register_InteractionCollection(m); diff --git a/projects/interactions/public/SIREN/interactions/CharmDISFromSpline.h b/projects/interactions/public/SIREN/interactions/CharmDISFromSpline.h new file mode 100644 index 000000000..80f97a386 --- /dev/null +++ b/projects/interactions/public/SIREN/interactions/CharmDISFromSpline.h @@ -0,0 +1,162 @@ +#pragma once +#ifndef SIREN_CharmDISFromSpline_H +#define SIREN_CharmDISFromSpline_H + +#include // for set +#include // for map +#include +#include // for vector +#include // for uint32_t +#include // for pair +#include +#include // for runtime... + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "SIREN/interactions/CrossSection.h" // for CrossSe... +#include "SIREN/dataclasses/InteractionSignature.h" // for Interac... +#include "SIREN/dataclasses/Particle.h" // for Particle + +namespace siren { namespace dataclasses { class InteractionRecord; } } +namespace siren { namespace utilities { class SIREN_random; } } + +namespace siren { +namespace interactions { + +class CharmDISFromSpline : public CrossSection { +friend cereal::access; +private: + photospline::splinetable<> differential_cross_section_; + photospline::splinetable<> total_cross_section_; + + std::vector signatures_; + std::set primary_types_; + std::set target_types_; + std::map> targets_by_primary_types_; + std::map, std::vector> signatures_by_parent_types_; + + int interaction_type_; + double target_mass_; + double minimum_Q2_; + + double unit; + +public: + CharmDISFromSpline(); + CharmDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minumum_Q2, std::set primary_types, std::set target_types, std::string units = "cm"); + CharmDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minumum_Q2, std::vector primary_types, std::vector target_types, std::string units = "cm"); + CharmDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minumum_Q2, std::set primary_types, std::set target_types, std::string units = "cm"); + CharmDISFromSpline(std::string differential_filename, std::string total_filename, std::set primary_types, std::set target_types, std::string units = "cm"); + CharmDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minumum_Q2, std::vector primary_types, std::vector target_types, std::string units = "cm"); + CharmDISFromSpline(std::string differential_filename, std::string total_filename, std::vector primary_types, std::vector target_types, std::string units = "cm"); + + void SetUnits(std::string units); + + virtual bool equal(CrossSection const & other) const override; + + double TotalCrossSection(dataclasses::InteractionRecord const &) const override; + double TotalCrossSection(siren::dataclasses::ParticleType primary, double energy) const; + double DifferentialCrossSection(dataclasses::InteractionRecord const &) const override; + double DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2=std::numeric_limits::quiet_NaN()) const; + double InteractionThreshold(dataclasses::InteractionRecord const &) const override; + void SampleFinalState(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr random) const override; + + std::vector GetPossibleTargets() const override; + std::vector GetPossibleTargetsFromPrimary(siren::dataclasses::ParticleType primary_type) const override; + std::vector GetPossiblePrimaries() const override; + std::vector GetPossibleSignatures() const override; + std::vector GetPossibleSignaturesFromParents(siren::dataclasses::ParticleType primary_type, siren::dataclasses::ParticleType target_type) const override; + + virtual double FinalStateProbability(dataclasses::InteractionRecord const & record) const override; + + void LoadFromFile(std::string differential_filename, std::string total_filename); + void LoadFromMemory(std::vector & differential_data, std::vector & total_data); + + double GetMinimumQ2() const {return minimum_Q2_;}; + double GetTargetMass() const {return target_mass_;}; + int GetInteractionType() const {return interaction_type_;}; + + static double GetLeptonMass(siren::dataclasses::ParticleType lepton_type); + +public: + virtual std::vector DensityVariables() const override; + template + void save(Archive & archive, std::uint32_t const version) const { + if(version == 0) { + splinetable_buffer buf; + buf.size = 0; + auto result_obj = differential_cross_section_.write_fits_mem(); + buf.data = result_obj.first; + buf.size = result_obj.second; + + std::vector diff_blob; + diff_blob.resize(buf.size); + std::copy((char*)buf.data, (char*)buf.data + buf.size, &diff_blob[0]); + + archive(::cereal::make_nvp("DifferentialCrossSectionSpline", diff_blob)); + + buf.size = 0; + result_obj = total_cross_section_.write_fits_mem(); + buf.data = result_obj.first; + buf.size = result_obj.second; + + std::vector total_blob; + total_blob.resize(buf.size); + std::copy((char*)buf.data, (char*)buf.data + buf.size, &total_blob[0]); + + archive(::cereal::make_nvp("TotalCrossSectionSpline", total_blob)); + archive(::cereal::make_nvp("PrimaryTypes", primary_types_)); + archive(::cereal::make_nvp("TargetTypes", target_types_)); + archive(::cereal::make_nvp("InteractionType", interaction_type_)); + archive(::cereal::make_nvp("TargetMass", target_mass_)); + archive(::cereal::make_nvp("MinimumQ2", minimum_Q2_)); + archive(::cereal::make_nvp("Unit", unit)); + archive(cereal::virtual_base_class(this)); + } else { + throw std::runtime_error("CharmDISFromSpline only supports version <= 0!"); + } + } + template + void load(Archive & archive, std::uint32_t version) { + if(version == 0) { + std::vector differential_data; + std::vector total_data; + archive(::cereal::make_nvp("DifferentialCrossSectionSpline", differential_data)); + archive(::cereal::make_nvp("TotalCrossSectionSpline", total_data)); + archive(::cereal::make_nvp("PrimaryTypes", primary_types_)); + archive(::cereal::make_nvp("TargetTypes", target_types_)); + archive(::cereal::make_nvp("InteractionType", interaction_type_)); + archive(::cereal::make_nvp("TargetMass", target_mass_)); + archive(::cereal::make_nvp("MinimumQ2", minimum_Q2_)); + archive(::cereal::make_nvp("Unit", unit)); + archive(cereal::virtual_base_class(this)); + LoadFromMemory(differential_data, total_data); + InitializeSignatures(); + } else { + throw std::runtime_error("CharmDISFromSpline only supports version <= 0!"); + } + } +private: + void ReadParamsFromSplineTable(); + void InitializeSignatures(); +}; + +} // namespace interactions +} // namespace siren + +CEREAL_CLASS_VERSION(siren::interactions::CharmDISFromSpline, 0); +CEREAL_REGISTER_TYPE(siren::interactions::CharmDISFromSpline); +CEREAL_REGISTER_POLYMORPHIC_RELATION(siren::interactions::CrossSection, siren::interactions::CharmDISFromSpline); + +#endif // SIREN_CharmDISFromSpline_H diff --git a/projects/interactions/public/SIREN/interactions/CharmHadronization.h b/projects/interactions/public/SIREN/interactions/CharmHadronization.h new file mode 100644 index 000000000..86b7cc839 --- /dev/null +++ b/projects/interactions/public/SIREN/interactions/CharmHadronization.h @@ -0,0 +1,82 @@ +#pragma once +#ifndef SIREN_CharmHadronization_H +#define SIREN_CharmHadronization_H + +#include // for shared_ptr +#include // for string +#include // for vector +#include // for uint32_t + +#include +#include +#include +#include +#include +#include + +#include "SIREN/interactions/Hadronization.h" // for Hadronization +#include "SIREN/dataclasses/Particle.h" // for Particle +#include "SIREN/dataclasses/InteractionSignature.h" // for InteractionSignature +#include "SIREN/dataclasses/InteractionRecord.h" // for InteractionSignature + +#include "SIREN/utilities/Random.h" // for SIREN_random +#include "SIREN/geometry/Geometry.h" +#include "SIREN/utilities/Constants.h" // for electronMass + + +namespace siren { namespace dataclasses { class InteractionRecord; } } +namespace siren { namespace dataclasses { struct InteractionSignature; } } +namespace siren { namespace utilities { class SIREN_random; } } + +namespace siren { +namespace interactions { + +class CharmHadronization : public Hadronization { +friend cereal::access; +private: + const std::set primary_types = {siren::dataclasses::Particle::ParticleType::Charm, siren::dataclasses::Particle::ParticleType::CharmBar}; + +public: + + CharmHadronization(); + + // virtual pybind11::object get_self(); + + virtual bool equal(Hadronization const & other) const override; + + void SampleFinalState(dataclasses::CrossSectionDistributionRecord & interaction, std::shared_ptr random) const override; + + virtual std::vector GetPossibleSignatures() const override; // Requires python-side implementation + virtual std::vector GetPossibleSignaturesFromParent(siren::dataclasses::Particle::ParticleType primary) const override; // Requires python-side implementation + + double FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const override; + + static double getHadronMass(siren::dataclasses::ParticleType hadron_type); + +public: + template + void save(Archive & archive, std::uint32_t const version) const { + if(version == 0) { + archive(::cereal::make_nvp("Hadronization", cereal::virtual_base_class(this))); + } else { + throw std::runtime_error("CharmHadronization only supports version <= 0!"); + } + } + template + void load_and_construct(Archive & archive, cereal::construct & construct, std::uint32_t version) { + if(version == 0) { + archive(::cereal::make_nvp("Hadronization", cereal::virtual_base_class(construct.ptr()))); + } else { + throw std::runtime_error("CharmHadronization only supports version <= 0!"); + } + } + +}; + +} // namespace interactions +} // namespace siren + +CEREAL_CLASS_VERSION(siren::interactions::CharmHadronization, 0); + + +#endif // SIREN_CharmHadronization_H diff --git a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h new file mode 100644 index 000000000..bcb66b6a2 --- /dev/null +++ b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h @@ -0,0 +1,89 @@ +#pragma once +#ifndef SIREN_CharmMesonDecay_H +#define SIREN_CharmMesonDecay_H + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "SIREN/dataclasses/Particle.h" +#include "SIREN/dataclasses/InteractionSignature.h" +#include "SIREN/dataclasses/InteractionRecord.h" + +#include "SIREN/interactions/Decay.h" +#include "SIREN/utilities/Interpolator.h" + + +namespace siren { +namespace interactions { + +class CharmMesonDecay : public Decay { +friend cereal::access; +private: + const std::set primary_types = {siren::dataclasses::Particle::ParticleType::D0, siren::dataclasses::Particle::ParticleType::DPlus}; + siren::utilities::Interpolator1D inverseCdf; // for dGamma +public: + CharmMesonDecay(); + CharmMesonDecay(siren::dataclasses::Particle::ParticleType primary); + virtual bool equal(Decay const & other) const override; + static double particleMass(siren::dataclasses::ParticleType particle); + double TotalDecayWidth(dataclasses::InteractionRecord const &) const override; + double TotalDecayWidth(siren::dataclasses::Particle::ParticleType primary) const override; + double TotalDecayWidthForFinalState(dataclasses::InteractionRecord const &) const override; + double DifferentialDecayWidth(dataclasses::InteractionRecord const &) const override; + double DifferentialDecayWidth(std::vector constants, double Q2, double mD, double mK) const; + void SampleFinalState(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr) const override; + std::vector GetPossibleSignatures() const override; + std::vector GetPossibleSignaturesFromParent(siren::dataclasses::Particle::ParticleType primary) const override; + virtual double FinalStateProbability(dataclasses::InteractionRecord const & record) const override; + std::vector FormFactorFromRecord(dataclasses::CrossSectionDistributionRecord const & record) const; + void computeDiffGammaCDF(std::vector constants, double mD, double mK); + +public: + virtual std::vector DensityVariables() const override; + template + void save(Archive & archive, std::uint32_t const version) const { + if(version == 0) { + archive(::cereal::make_nvp("PrimaryTypes", primary_types)); + archive(::cereal::make_nvp("Decay", cereal::virtual_base_class(this))); + } else { + throw std::runtime_error("CharmMesonDecay only supports version <= 0!"); + } + } + template + void load_and_construct(Archive & archive, cereal::construct & construct, std::uint32_t version) { + if(version == 0) { + std::set _primary_types; + + archive(::cereal::make_nvp("PrimaryTypes", _primary_types)); + construct(_primary_types); + archive(::cereal::make_nvp("Decay", cereal::virtual_base_class(construct.ptr()))); + } else { + throw std::runtime_error("CharmMesonDecay only supports version <= 0!"); + } + } + +}; + +} // namespace interactions +} // namespace siren + +CEREAL_CLASS_VERSION(siren::interactions::CharmMesonDecay, 0); +CEREAL_REGISTER_TYPE(siren::interactions::CharmMesonDecay); +CEREAL_REGISTER_POLYMORPHIC_RELATION(siren::interactions::Decay, siren::interactions::CharmMesonDecay); + +#endif // SIREN_CharmMesonDecay_H diff --git a/projects/interactions/public/SIREN/interactions/DMesonELoss.h b/projects/interactions/public/SIREN/interactions/DMesonELoss.h new file mode 100644 index 000000000..d6d5097d3 --- /dev/null +++ b/projects/interactions/public/SIREN/interactions/DMesonELoss.h @@ -0,0 +1,96 @@ +#pragma once +#ifndef SIREN_DMesonELoss_H +#define SIREN_DMesonELoss_H + +#include // for set +#include // for map +#include +#include // for vector +#include // for uint32_t +#include // for pair +#include +#include // for runtime... + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "SIREN/interactions/CrossSection.h" // for CrossSe... +#include "SIREN/dataclasses/InteractionSignature.h" // for Interac... +#include "SIREN/dataclasses/Particle.h" // for Particle + +namespace siren { namespace dataclasses { class InteractionRecord; } } +namespace siren { namespace utilities { class SIREN_random; } } + +namespace siren { +namespace interactions { + +class DMesonELoss : public CrossSection { +friend cereal::access; +private: + std::set primary_types_ = {siren::dataclasses::Particle::ParticleType::D0, siren::dataclasses::Particle::ParticleType::DPlus}; + std::set target_types_ = {siren::dataclasses::Particle::ParticleType::PPlus}; + +public: + DMesonELoss(); + + virtual bool equal(CrossSection const & other) const override; + + double TotalCrossSection(dataclasses::InteractionRecord const &) const override; + double TotalCrossSection(siren::dataclasses::Particle::ParticleType primary, double energy) const; + // double TotalCrossSection(siren::dataclasses::Particle::ParticleType primary, double energy, siren::dataclasses::Particle::ParticleType target) const override; + + double DifferentialCrossSection(dataclasses::InteractionRecord const &) const override; + double InteractionThreshold(dataclasses::InteractionRecord const &) const override; + void SampleFinalState(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr random) const override; + + std::vector GetPossibleTargets() const override; + std::vector GetPossibleTargetsFromPrimary(siren::dataclasses::Particle::ParticleType primary_type) const override; + std::vector GetPossiblePrimaries() const override; + std::vector GetPossibleSignatures() const override; + std::vector GetPossibleSignaturesFromParents(siren::dataclasses::Particle::ParticleType primary_type, siren::dataclasses::Particle::ParticleType target_type) const override; + + virtual double FinalStateProbability(dataclasses::InteractionRecord const & record) const override; + +public: + virtual std::vector DensityVariables() const override; + template + void save(Archive & archive, std::uint32_t const version) const { + if(version == 0) { + archive(::cereal::make_nvp("PrimaryTypes", primary_types_)); + archive(cereal::virtual_base_class(this)); + } else { + throw std::runtime_error("DMesonELoss only supports version <= 0!"); + } + } + template + void load(Archive & archive, std::uint32_t version) { + if(version == 0) { + archive(::cereal::make_nvp("PrimaryTypes", primary_types_)); + archive(cereal::virtual_base_class(this)); + InitializeSignatures(); + } else { + throw std::runtime_error("DMesonELoss only supports version <= 0!"); + } + } +private: + void InitializeSignatures(); +}; + +} // namespace interactions +} // namespace siren + +CEREAL_CLASS_VERSION(siren::interactions::DMesonELoss, 0); +CEREAL_REGISTER_TYPE(siren::interactions::DMesonELoss); +CEREAL_REGISTER_POLYMORPHIC_RELATION(siren::interactions::CrossSection, siren::interactions::DMesonELoss); + +#endif // SIREN_DMesonELoss_H diff --git a/projects/interactions/public/SIREN/interactions/Hadronization.h b/projects/interactions/public/SIREN/interactions/Hadronization.h new file mode 100644 index 000000000..6f9312769 --- /dev/null +++ b/projects/interactions/public/SIREN/interactions/Hadronization.h @@ -0,0 +1,58 @@ +#pragma once +#ifndef SIREN_Hadronization_H +#define SIREN_Hadronization_H + +#include // for shared_ptr +#include // for string +#include // for vector +#include // for uint32_t + +#include +#include +#include +#include +#include +#include + +#include "SIREN/dataclasses/Particle.h" // for Particle +#include "SIREN/dataclasses/InteractionSignature.h" // for InteractionSignature +#include "SIREN/dataclasses/InteractionRecord.h" // for InteractionSignature + +#include "SIREN/utilities/Random.h" // for SIREN_random +#include "SIREN/geometry/Geometry.h" + +namespace siren { namespace dataclasses { class InteractionRecord; } } +namespace siren { namespace dataclasses { struct InteractionSignature; } } +namespace siren { namespace utilities { class SIREN_random; } } + +namespace siren { +namespace interactions { + +class Hadronization { +friend cereal::access; +private: +public: + Hadronization(); + virtual ~Hadronization() {}; + bool operator==(Hadronization const & other) const; + virtual bool equal(Hadronization const & other) const = 0; + + virtual void SampleFinalState(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr) const = 0; + virtual std::vector GetPossibleSignatures() const = 0; + virtual std::vector GetPossibleSignaturesFromParent(siren::dataclasses::Particle::ParticleType primary) const = 0; + virtual double FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const = 0; + + template + void save(Archive & archive, std::uint32_t const version) const {}; + template + void load(Archive & archive, std::uint32_t const version) {}; + +}; // class Hadronization + +} // namespace interactions +} // namespace siren + +CEREAL_CLASS_VERSION(siren::interactions::Hadronization, 0); + + +#endif // SIREN_Hadronization_H diff --git a/projects/interactions/public/SIREN/interactions/InteractionCollection.h b/projects/interactions/public/SIREN/interactions/InteractionCollection.h index 3a8e8f80b..287b87a59 100644 --- a/projects/interactions/public/SIREN/interactions/InteractionCollection.h +++ b/projects/interactions/public/SIREN/interactions/InteractionCollection.h @@ -21,12 +21,14 @@ #include #include #include -#include +#include #include #include "SIREN/dataclasses/Particle.h" // for Particle #include "SIREN/interactions/CrossSection.h" #include "SIREN/interactions/Decay.h" +#include "SIREN/interactions/Hadronization.h" + namespace siren { namespace dataclasses { class InteractionRecord; } } @@ -38,6 +40,8 @@ class InteractionCollection { siren::dataclasses::ParticleType primary_type; std::vector> cross_sections; std::vector> decays; + std::vector> hadronizations; + std::map>> cross_sections_by_target; std::set target_types; static const std::vector> empty; @@ -47,12 +51,21 @@ class InteractionCollection { virtual ~InteractionCollection() {}; InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> cross_sections); InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> decays); + InteractionCollection(siren::dataclasses::Particle::ParticleType primary_type, std::vector> hadronizations); + InteractionCollection(siren::dataclasses::Particle::ParticleType primary_type, std::vector> decays, std::vector> hadronizations); + InteractionCollection(siren::dataclasses::Particle::ParticleType primary_type, std::vector> cross_sections, std::vector> hadronizations); + InteractionCollection(siren::dataclasses::Particle::ParticleType primary_type, std::vector> cross_sections, std::vector> decays, std::vector> hadronizations); + InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> cross_sections, std::vector> decays); bool operator==(InteractionCollection const & other) const; std::vector> const & GetCrossSections() const {return cross_sections;} std::vector> const & GetDecays() const {return decays;} + std::vector> const & GetHadronizations() const {return hadronizations;} + bool const HasCrossSections() const {return cross_sections.size() > 0;} bool const HasDecays() const {return decays.size() > 0;} + bool const HasHadronizations() const {return hadronizations.size() > 0;} + std::vector> const & GetCrossSectionsForTarget(siren::dataclasses::ParticleType p) const; std::map>> const & GetCrossSectionsByTarget() const { return cross_sections_by_target; @@ -73,6 +86,7 @@ class InteractionCollection { archive(cereal::make_nvp("TargetTypes", target_types)); archive(cereal::make_nvp("CrossSections", cross_sections)); archive(cereal::make_nvp("Decays", decays)); + archive(cereal::make_nvp("Hadronizations", hadronizations)); } else { throw std::runtime_error("InteractionCollection only supports version <= 0!"); } @@ -85,6 +99,7 @@ class InteractionCollection { archive(cereal::make_nvp("TargetTypes", target_types)); archive(cereal::make_nvp("CrossSections", cross_sections)); archive(cereal::make_nvp("Decays", decays)); + archive(cereal::make_nvp("Hadronizations", hadronizations)); InitializeTargetTypes(); } else { throw std::runtime_error("InteractionCollection only supports version <= 0!"); diff --git a/projects/utilities/public/SIREN/utilities/Constants.h b/projects/utilities/public/SIREN/utilities/Constants.h index bf7c29190..f01cb1567 100644 --- a/projects/utilities/public/SIREN/utilities/Constants.h +++ b/projects/utilities/public/SIREN/utilities/Constants.h @@ -53,6 +53,9 @@ static const double EtaMass = 0.547862; // GeV static const double EtaPrimeMass = 0.95778; // GeV static const double RhoMass = 0.77526; // GeV static const double OmegaMass = 0.78266; // GeV +static const double D0Mass = 1.86962; // GeV +static const double DPlusMass = 1.86484; // GeV +static const double CharmMass = 1.27; // GeV // confusing units // static const double second = 1.523e15; // [eV^-1 sec^-1] @@ -103,6 +106,9 @@ static const double gravConstant = 6.6700e-11; // [m^3 kg^-1 s^-2] static const double fineStructure = 1.0/137.0; // dimensionless static const double hbarc = 197.3*(1e-9)*(1e-7)*GeV*cm; // [GeV m] +//hbar +static const double hbar = 6.58211957 * (1e-25); // GeV seconds + } // namespace Constants } // namespace utilities diff --git a/python/SIREN_Controller.py b/python/SIREN_Controller.py index 7f714b115..df7854cd4 100644 --- a/python/SIREN_Controller.py +++ b/python/SIREN_Controller.py @@ -580,7 +580,7 @@ def SaveEvents(self, filename, fill_tables_at_exit=True, datasets["num_interactions"].append(id+1) # save injector and weighter - self.injector.SaveInjector(filename) + # self.injector.SaveInjector(filename) # weighter saving not yet supported #self.weighter.SaveWeighter(filename) From f1fb7cfb5167b4db7d10c6b928a71338a274fa32 Mon Sep 17 00:00:00 2001 From: MiaochenJin Date: Wed, 12 Jun 2024 15:24:07 -0400 Subject: [PATCH 02/93] add examples --- resources/Examples/DMesonExample/DIS_D.py | 110 +++++++++ .../Examples/DMesonExample/make_plots.py | 216 ++++++++++++++++++ .../Examples/DMesonExample/paper.mplstyle | 30 +++ .../Examples/DMesonExample/parse_output.py | 96 ++++++++ 4 files changed, 452 insertions(+) create mode 100644 resources/Examples/DMesonExample/DIS_D.py create mode 100644 resources/Examples/DMesonExample/make_plots.py create mode 100644 resources/Examples/DMesonExample/paper.mplstyle create mode 100644 resources/Examples/DMesonExample/parse_output.py diff --git a/resources/Examples/DMesonExample/DIS_D.py b/resources/Examples/DMesonExample/DIS_D.py new file mode 100644 index 000000000..3114111ca --- /dev/null +++ b/resources/Examples/DMesonExample/DIS_D.py @@ -0,0 +1,110 @@ +import os + +import siren +from siren.SIREN_Controller import SIREN_Controller + +# Number of events to inject +events_to_inject = 50000 + +# Expeirment to run +experiment = "IceCube" + +# Define the controller +controller = SIREN_Controller(events_to_inject, experiment, seed = 1) + +# Particle to inject +primary_type = siren.dataclasses.Particle.ParticleType.NuMu + +cross_section_model = "CSMSDISSplines" + +xsfiledir = siren.utilities.get_cross_section_model_path(cross_section_model) + +# Cross Section Model +target_type = siren.dataclasses.Particle.ParticleType.Nucleon + +DIS_xs = siren.interactions.CharmDISFromSpline( + os.path.join(xsfiledir, "dsdxdy_nu_CC_iso.fits"), + os.path.join(xsfiledir, "sigma_nu_CC_iso.fits"), + [primary_type], + [target_type], "m" +) + +primary_xs = siren.interactions.InteractionCollection(primary_type, [DIS_xs]) +controller.SetInteractions(primary_xs) + +# Primary distributions +primary_injection_distributions = {} +primary_physical_distributions = {} + +mass_dist = siren.distributions.PrimaryMass(0) +primary_injection_distributions["mass"] = mass_dist +primary_physical_distributions["mass"] = mass_dist + +# energy distribution +edist = siren.distributions.PowerLaw(2, 1e5, 1e10) +primary_injection_distributions["energy"] = edist +primary_physical_distributions["energy"] = edist + +# direction distribution +direction_distribution = siren.distributions.IsotropicDirection() +primary_injection_distributions["direction"] = direction_distribution +primary_physical_distributions["direction"] = direction_distribution + +# position distribution +muon_range_func = siren.distributions.LeptonDepthFunction() +position_distribution = siren.distributions.ColumnDepthPositionDistribution( + 600, 600.0, muon_range_func, set(controller.GetDetectorModelTargets()[0]) +) +primary_injection_distributions["position"] = position_distribution + +# SetProcesses +controller.SetProcesses( + primary_type, primary_injection_distributions, primary_physical_distributions +) + +def add_secondary_to_controller(controller, secondary_type, secondary_xsecs, secondary_decays = None): + if secondary_decays is not None: + secondary_collection = siren.interactions.InteractionCollection(secondary_type, [secondary_xsecs], [secondary_decays]) + else: + secondary_collection = siren.interactions.InteractionCollection(secondary_type, [secondary_xsecs]) + secondary_injection_process = siren.injection.SecondaryInjectionProcess() + secondary_physical_process = siren.injection.PhysicalProcess() + secondary_injection_process.primary_type = secondary_type + secondary_physical_process.primary_type = secondary_type + secondary_injection_process.AddSecondaryInjectionDistribution(siren.distributions.SecondaryPhysicalVertexDistribution()) + controller.secondary_injection_processes.append(secondary_injection_process) + controller.secondary_physical_processes.append(secondary_physical_process) + + return secondary_collection + +# secondary interactions +charms = siren.dataclasses.Particle.ParticleType.Charm +DPlus = siren.dataclasses.Particle.ParticleType.DPlus +D0 = siren.dataclasses.Particle.ParticleType.D0 +charm_hadronization = siren.interactions.CharmHadronization() +DPlus_decay = siren.interactions.CharmMesonDecay(primary_type = DPlus) +D0_decay = siren.interactions.CharmMesonDecay(primary_type = D0) +D_energy_loss = siren.interactions.DMesonELoss() + +secondary_charm_collection = add_secondary_to_controller(controller, charms, charm_hadronization) +secondary_DPlus_collection = add_secondary_to_controller(controller, DPlus, D_energy_loss, DPlus_decay) +secondary_D0_collection = add_secondary_to_controller(controller, D0, D_energy_loss, D0_decay) + +controller.SetInteractions(primary_xs, [secondary_charm_collection, secondary_D0_collection, secondary_DPlus_collection]) + +controller.Initialize() + +# def stop(datum, i): +# secondary_type = datum.record.signature.secondary_types[i] +# return ((secondary_type != siren.dataclasses.Particle.ParticleType.Charm) and (secondary_type != siren.dataclasses.Particle.ParticleType.DPlus)) + +def stop(datum, i): + return False + +controller.SetInjectorStoppingCondition(stop) + +events = controller.GenerateEvents() + +os.makedirs("output", exist_ok=True) + +controller.SaveEvents("output/FullSim") diff --git a/resources/Examples/DMesonExample/make_plots.py b/resources/Examples/DMesonExample/make_plots.py new file mode 100644 index 000000000..c8b246aa9 --- /dev/null +++ b/resources/Examples/DMesonExample/make_plots.py @@ -0,0 +1,216 @@ +import h5py +import numpy as np +from matplotlib import pyplot as plt +from matplotlib.colors import LogNorm +from mpl_toolkits.axes_grid1 import make_axes_locatable +from parse_output import analysis +filename = "output/FullSim.parquet" +plt.style.use('paper.mplstyle') + + +sim = analysis(filename) + + +c = 3 * 1e8 # m/s +m_D0 = 1.86962 # GeV +m_Dp = 1.86484 +t_Dp = 1040 * 1e-15 # s +t_D0 = 410 * 1e-15 +m_ice = 18.02 # g/mol +N = 6.02214 * 1e23 #mol^-1 +rho = 0.917 # g/cm^3 + + +def normalize(hist, xbins, ybins): + normed_hist = np.zeros_like(hist) + for i in range(len(xbins) - 1): + tot = 0 + for j in range(len(ybins) - 1): + tot += hist[i][j] + for j in range(len(ybins) - 1): + if tot != 0: + normed_hist[i][j] = hist[i][j] / tot + else: + normed_hist[i][j] = 0 + return normed_hist + +def analytic_decay_length(E, t, m): + return E * t / ((m/(c ** 2)) * c) + +def xsec(E): + return (np.exp(1.891 + 0.2095 * np.log10(E)) - 2.157 + 1.263 * np.log10(E)) * 1e-27 # convert to cm^2 + +def analytic_free_path(E): + return (m_ice / (rho * N * xsec(E))) / 100 # convert to m + +def plot_separation_distribution(analysis_): + D0_energies, D0_separations, Dp_energies, Dp_separations = analysis_.separation_analysis() + min_eng = 1e1 + max_eng = 1e9 + energy_bins = np.logspace(np.log10(min_eng), np.log10(max_eng), 20) + + energy_bins_centers = np.zeros((len(energy_bins) - 1,)) + for i in range(len(energy_bins_centers)): + energy_bins_centers[i] = np.sqrt(energy_bins[i] * energy_bins[i + 1]) + D0_analytic_lengths = analytic_decay_length(energy_bins_centers, t_D0, m_D0) + Dp_analytic_lengths = analytic_decay_length(energy_bins_centers, t_Dp, m_Dp) + + min_sep = 1e-3 + max_sep = 50000 + log_separation_bins = np.logspace(np.log10(min_sep), np.log10(max_sep), 20) + + X2, Y2 = np.meshgrid(energy_bins, log_separation_bins) + log_hist_D0, _, _ = np.histogram2d(D0_energies, D0_separations, bins = (energy_bins, log_separation_bins)) + log_hist_Dp, _, _ = np.histogram2d(Dp_energies, Dp_separations, bins = (energy_bins, log_separation_bins)) + log_hist_D0 = normalize(log_hist_D0, energy_bins, log_separation_bins) + log_hist_Dp = normalize(log_hist_Dp, energy_bins, log_separation_bins) + + fig, axes = plt.subplots(nrows = 1, ncols = 2, figsize = (11, 5)) + + log_im1 = axes[0].pcolor(X2, Y2, log_hist_D0.T, cmap="plasma", alpha = 0.7, vmin=0, vmax=1) + log_im2 = axes[1].pcolor(X2, Y2, log_hist_Dp.T, cmap="plasma", alpha = 0.7, vmin=0, vmax=1) + + # divider1 = make_axes_locatable(axes[0]) + # cax1 = divider1.append_axes('right', size='5%', pad=0.05) + divider2 = make_axes_locatable(axes[1]) + cax2 = divider2.append_axes('right', size='5%', pad=0.05) + fig.colorbar(log_im2, cax=cax2, orientation='vertical', alpha = 0.7) + # fig.colorbar(log_im1, cax=cax1, orientation='vertical', alpha = 0.7) + + axes[0].set_title(r"$D^0$ Separation") + axes[1].set_title(r"$D^+$ Separation") + + axes[0].set_xlabel(r"$E_{D^0}$ [GeV]") + axes[1].set_xlabel(r"$E_{D^+}$ [GeV]") + + axes[0].set_ylabel("Separation Length [m]") + + axes[0].set_xscale('log') + axes[1].set_xscale('log') + axes[0].set_yscale('log') + axes[1].set_yscale('log') + + axes[0].set_ylim(min_sep, max_sep) + axes[1].set_ylim(min_sep, max_sep) + axes[0].set_xlim(min_eng, max_eng) + axes[1].set_xlim(min_eng, max_eng) + + # also plot the analytic lines + axes[0].plot(energy_bins_centers, D0_analytic_lengths, color = '#FEF3E8', alpha = 0.7) + axes[1].plot(energy_bins_centers, Dp_analytic_lengths, label = r"$d = \frac{E \tau}{mc}$", color = '#FEF3E8', alpha = 0.7) + + legend = axes[1].legend(loc = 'upper left') + for text in legend.get_texts(): + text.set_color('#FEF3E8') + + fig.savefig("./plots/Separation_Length_Distribution", bbox_inches = 'tight') + +# plot_separation_distribution(sim) + +def plot_2d_energy_loss(analysis_): + E_D0, E_Dp, n_D0, n_Dp = analysis_.energy_loss_analysis_2d() + energy_bins = np.logspace(np.log10(min(min(E_D0), min(E_Dp))), np.log10(max(max(E_D0), max(E_Dp))), 20) + num_bins = np.linspace(-0.01, 7.99, 9) + X, Y = np.meshgrid(energy_bins, num_bins) + hist_D0, _, _ = np.histogram2d(E_D0, n_D0, bins = (energy_bins, num_bins)) + hist_Dp, _, _ = np.histogram2d(E_Dp, n_Dp, bins = (energy_bins, num_bins)) + + hist_D0 = normalize(hist_D0, energy_bins, num_bins) + hist_Dp = normalize(hist_Dp, energy_bins, num_bins) + + fig, axes = plt.subplots(nrows = 1, ncols = 2, figsize = (11, 5)) + im1 = axes[0].pcolor(X, Y, hist_D0.T, cmap="plasma", alpha = 0.7) + im2 = axes[1].pcolor(X, Y, hist_Dp.T, cmap="plasma", alpha = 0.7) + divider2 = make_axes_locatable(axes[1]) + cax2 = divider2.append_axes('right', size='5%', pad=0.05) + fig.colorbar(im2, cax=cax2, orientation='vertical', alpha = 0.7) + axes[0].axvline(x = 53 * 1e3, color = '#FEF3E8', alpha = 0.7, label = r"$d_{D^0} = l_{D^0}$") + axes[1].axvline(x = 22 * 1e3, color = '#FEF3E8', alpha = 0.7, label = r"$d_{D^+} = l_{D^+}$") + + axes[0].set_title(r"$D^0-p$ Collision") + axes[1].set_title(r"$D^+-p$ Collision") + + axes[0].set_xlabel(r"$E_{D^0}$ [GeV]") + axes[1].set_xlabel(r"$E_{D^+}$ [GeV]") + + axes[0].set_ylim(0, 8) + axes[1].set_ylim(0, 8) + + axes[0].set_ylabel(r"$n_{\textrm{Elastic Collision}}$") + axes[0].set_xscale('log') + axes[1].set_xscale('log') + legend0 = axes[0].legend() + legend1 = axes[1].legend() + for text in legend0.get_texts(): + text.set_color('#FEF3E8') + for text in legend1.get_texts(): + text.set_color('#FEF3E8') + + + fig.savefig("./plots/Energy_loss_2d_Distribution", bbox_inches = 'tight') + +plot_2d_energy_loss(sim) +exit(0) + +def plot_free_path_distribution(): + D0_E_list, D0_free_path_list, Dp_E_list, Dp_free_path_list = analysis_.free_path_analysis() + energy_bins = np.logspace(1.5, 9, 20) + distance_bins = np.logspace(-3, np.log10(5000), 20) + X, Y = np.meshgrid(energy_bins, distance_bins) + hist_D0, _, _ = np.histogram2d(D0_E_list, D0_free_path_list, bins = (energy_bins, distance_bins)) + hist_Dp, _, _ = np.histogram2d(Dp_E_list, Dp_free_path_list, bins = (energy_bins, distance_bins)) + + hist_D0 = normalize(hist_D0, energy_bins, distance_bins) + hist_Dp = normalize(hist_Dp, energy_bins, distance_bins) + + energy_bins_centers = np.zeros((len(energy_bins) - 1,)) + for i in range(len(energy_bins_centers)): + energy_bins_centers[i] = np.sqrt(energy_bins[i] * energy_bins[i + 1]) + D0_analytic_lengths = analytic_free_path(energy_bins_centers) + Dp_analytic_lengths = analytic_free_path(energy_bins_centers) + + + fig, axes = plt.subplots(nrows = 1, ncols = 2, figsize = (11, 5)) + im1 = axes[0].pcolor(X, Y, hist_D0.T, cmap="plasma", alpha = 0.7, vmin=0, vmax=1) + im2 = axes[1].pcolor(X, Y, hist_Dp.T, cmap="plasma", alpha = 0.7, vmin=0, vmax=1) + divider2 = make_axes_locatable(axes[1]) + cax2 = divider2.append_axes('right', size='5%', pad=0.05) + fig.colorbar(im2, cax=cax2, orientation='vertical', alpha = 0.7) + + axes[0].set_title(r"$D^0-p$ Free Path") + axes[1].set_title(r"$D^+-p$ Free Path") + + axes[0].set_xlabel(r"$E_{D^0}$ [GeV]") + axes[1].set_xlabel(r"$E_{D^+}$ [GeV]") + + axes[0].plot(energy_bins_centers, D0_analytic_lengths, color = '#FEF3E8', alpha = 0.7) + axes[1].plot(energy_bins_centers, Dp_analytic_lengths, label = r"$l = \frac{m_{\textrm{ice}}}{\rho N_A \sigma(E)}$", color = '#FEF3E8', alpha = 0.7) + + # also plot the decay lengths to explain low energy increase + D0_decay_analytic_lengths = analytic_decay_length(energy_bins_centers, t_D0, m_D0) + Dp_decay_analytic_lengths = analytic_decay_length(energy_bins_centers, t_Dp, m_Dp) + + axes[0].plot(energy_bins_centers, D0_decay_analytic_lengths, label = r"$d = \frac{E \tau}{mc}$", color = '#A597B6', alpha = 0.7) + axes[1].plot(energy_bins_centers, Dp_decay_analytic_lengths, color = '#A597B6', alpha = 0.7) + + axes[0].set_ylabel(r"$l_{\textrm{Free}}$") + axes[0].set_xscale('log') + axes[1].set_xscale('log') + axes[0].set_yscale('log') + axes[1].set_yscale('log') + + axes[0].set_ylim(1e-3, 5000) + axes[1].set_ylim(1e-3, 5000) + + legend0 = axes[0].legend(loc = 'upper left') + for text in legend0.get_texts(): + text.set_color('#A597B6') + + legend1 = axes[1].legend(loc = 'upper left') + for text in legend1.get_texts(): + text.set_color('#FEF3E8') + + fig.savefig("./plots/Free_Path_Distribution", bbox_inches = 'tight') + return + +plot_free_path_distribution() \ No newline at end of file diff --git a/resources/Examples/DMesonExample/paper.mplstyle b/resources/Examples/DMesonExample/paper.mplstyle new file mode 100644 index 000000000..77b5af0b3 --- /dev/null +++ b/resources/Examples/DMesonExample/paper.mplstyle @@ -0,0 +1,30 @@ +figure.figsize : 5, 5 # figure size in inches +savefig.dpi : 600 # figure dots per inch + +font.size: 20 +font.family: serif +font.serif: Computer Modern, Latin Modern Roman, Bitstream Vera Serif +text.usetex: True + +axes.prop_cycle: cycler('color', ['29A2C6','FF6D31','73B66B','9467BD','FFCB18', 'EF597B']) +axes.grid: False + +image.cmap : plasma + +lines.linewidth: 2 +patch.linewidth: 2 +xtick.labelsize: large +ytick.labelsize: large +xtick.minor.visible: True # visibility of minor ticks on x-axis +ytick.minor.visible: True # visibility of minor ticks on y-axis +xtick.major.size: 6 # major tick size in points +xtick.minor.size: 3 # minor tick size in points +ytick.major.size: 6 # major tick size in points +ytick.minor.size: 3 # minor tick size in points +xtick.major.width: 1 +xtick.minor.width: 1 +ytick.major.width: 1 +ytick.minor.width: 1 + +legend.frameon: False +legend.fontsize: 16 diff --git a/resources/Examples/DMesonExample/parse_output.py b/resources/Examples/DMesonExample/parse_output.py new file mode 100644 index 000000000..32a316953 --- /dev/null +++ b/resources/Examples/DMesonExample/parse_output.py @@ -0,0 +1,96 @@ +import pandas as pd +import h5py +import numpy as np +from matplotlib import pyplot as plt +from matplotlib import pyplot as plt +from matplotlib.colors import LogNorm +from mpl_toolkits.axes_grid1 import make_axes_locatable + +filename = "output/FullSim.parquet" + +def normalize(hist, xbins, ybins): + normed_hist = np.zeros_like(hist) + for i in range(len(xbins) - 1): + tot = 0 + for j in range(len(ybins) - 1): + tot += hist[i][j] + for j in range(len(ybins) - 1): + if tot != 0: + normed_hist[i][j] = hist[i][j] / tot + else: + normed_hist[i][j] = 0 + return normed_hist + +def mass(p): + return np.sqrt(p[0] ** 2 - (p[1] ** 2 + p[2] ** 2 + p[3] ** 2)) + +def decay_length(v1, v2): + return np.sqrt((v1[0] - v2[0]) ** 2 + (v1[1] - v2[1]) ** 2 + (v1[2] - v2[2]) ** 2) + +def extract_Q2(pe, pnu): + return (pe[0] + pnu[0]) ** 2 - ((pe[1] + pnu[1]) ** 2 + (pe[2] + pnu[2]) ** 2 + (pe[3] + pnu[3]) ** 2) + +def add_to_dict(list, dictionary): + for item in list: + if item in dictionary: + dictionary[item] += 1 + else: dictionary[item] = 1 + +class event: + def __init__(self, row) -> None: + self.row = row + self.num_interaction = row["num_interactions"] + + def DTYPE(self) -> int: + # type of charmed meson is the second one + return int(self.row["secondary_types"][1][1]) + + def D_DECAY_LEN(self) -> float: + # the first vertex is 0th, the decay vertex is the last one + return decay_length(self.row["vertex"][0], self.row["vertex"][-1]) + +class analysis: + def __init__(self, f) -> None: + self.df = pd.read_parquet(f) + self.num_events = len(self.df["event_weight"]) + # print("Initializing... There are {} events".format(f.attrs["num_events"])) + self.num_interactions = {} + self.secondary_types = {} + + def separation_analysis(self): + D0_energies = [] + D0_separations = [] + Dp_energies = [] + Dp_separations = [] + for i in range(self.num_events): + cur_event = event(self.df.iloc[i]) + print("{}/{}".format(i, self.num_events), end = '\r') + # check if current event is D0 or D+ + if cur_event.DTYPE() == 421: # This is D0 + # extract the vertex separations + D0_separations.append(cur_event.D_DECAY_LEN()) + D0_energies.append(cur_event.row["primary_momentum"][2][0]) + elif cur_event.DTYPE() == 411: # This is D+ + # extract the vertex separations + Dp_separations.append(cur_event.D_DECAY_LEN()) + Dp_energies.append(cur_event.row["primary_momentum"][2][0]) + return D0_energies, D0_separations, Dp_energies, Dp_separations + + def energy_loss_analysis_2d(self): + E_D0 = [] + E_Dp = [] + n_D0 = [] + n_Dp = [] + for i in range(self.num_events): + cur_event = event(self.df.iloc[i]) + print("{}/{}".format(i, self.num_events), end = '\r') + if cur_event.DTYPE() == 421: # This is D0 + # extract the vertex separations + n_D0.append(cur_event.row["num_interactions"] - 3) + E_D0.append(cur_event.row["primary_momentum"][2][0]) + elif cur_event.DTYPE() == 411: # This is D+ + # extract the vertex separations + n_Dp.append(cur_event.row["num_interactions"] - 3) + E_Dp.append(cur_event.row["primary_momentum"][2][0]) + return E_D0, E_Dp, n_D0, n_Dp + From 3ce2b8ff13e173bd1f8d8e6742722b215ce51b59 Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Thu, 22 Aug 2024 15:16:01 -0400 Subject: [PATCH 03/93] preparing for PR --- projects/detector/private/DetectorModel.cxx | 11 +- .../SecondaryBoundedVertexDistribution.cxx | 4 +- .../SecondaryPhysicalVertexDistribution.cxx | 12 +- .../SecondaryVertexPositionDistribution.cxx | 2 +- projects/injection/private/Injector.cxx | 119 +++++++++++++---- .../private/CharmDISFromSpline.cxx | 87 +++++++++++-- .../private/CharmHadronization.cxx | 120 +++++++++++++----- .../SIREN/interactions/CharmHadronization.h | 15 ++- python/SIREN_Controller.py | 11 +- resources/Examples/DMesonExample/DIS_D.py | 2 +- .../Examples/DMesonExample/make_plots.py | 118 +++++++++++------ .../Examples/DMesonExample/parse_output.py | 16 ++- 12 files changed, 391 insertions(+), 126 deletions(-) diff --git a/projects/detector/private/DetectorModel.cxx b/projects/detector/private/DetectorModel.cxx index 3bdc63051..e8cbcffb6 100644 --- a/projects/detector/private/DetectorModel.cxx +++ b/projects/detector/private/DetectorModel.cxx @@ -683,7 +683,8 @@ double DetectorModel::GetInteractionDensity(Geometry::IntersectionList const & i std::vector const & total_cross_sections, double const & total_decay_length) const { Vector3D direction = p0 - intersections.position; - if(direction.magnitude() == 0) { + // std::cout << "direction: " << direction.magnitude() << std::endl; + if(direction.magnitude() == 0 || direction.magnitude() <= 1e-05) { direction = intersections.direction; } else { direction.normalize(); @@ -978,18 +979,26 @@ double DetectorModel::GetInteractionDepthInCGS(Geometry::IntersectionList const std::vector const & targets, std::vector const & total_cross_sections, double const & total_decay_length) const { + + // std::cout << p0 << " " << p1 << " " << intersections.direction << std::endl; if(p0 == p1) { return 0.0; } Vector3D direction = p1 - p0; double distance = direction.magnitude(); + // std::cout << "distance is " << distance << std::endl; if(distance == 0.0) { return 0.0; } + if(direction.magnitude() <= 1e-05) { + // std::cout << "triggered" << std::endl; + return 0.0; + } direction.normalize(); double dot = intersections.direction * direction; assert(std::abs(1.0 - std::abs(dot)) < 1e-6); + // std::cout << "not at this point" << std::endl; double offset = (intersections.position - p0) * direction; if(dot < 0) { diff --git a/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx b/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx index ca3c407cd..a86fffb1c 100644 --- a/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx +++ b/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx @@ -53,7 +53,7 @@ double log_one_minus_exp_of_negative(double x) { void SecondaryBoundedVertexDistribution::SampleVertex(std::shared_ptr rand, std::shared_ptr detector_model, std::shared_ptr interactions, siren::dataclasses::SecondaryDistributionRecord & record) const { - std::cout << "in sample bounded vertex" << std::endl; + // std::cout << "in sample bounded vertex" << std::endl; siren::math::Vector3D pos = record.initial_position; siren::math::Vector3D dir = record.direction; @@ -123,7 +123,7 @@ void SecondaryBoundedVertexDistribution::SampleVertex(std::shared_ptr detector_model, std::shared_ptr interactions, siren::dataclasses::InteractionRecord const & record) const { - std::cout << "in sample bounded vertex gen prob" << std::endl; + // std::cout << "in sample bounded vertex gen prob" << std::endl; siren::math::Vector3D dir(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]); dir.normalize(); diff --git a/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx b/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx index f9fb53b41..d49a42f03 100644 --- a/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx +++ b/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx @@ -51,20 +51,20 @@ double log_one_minus_exp_of_negative(double x) { void SecondaryPhysicalVertexDistribution::SampleVertex(std::shared_ptr rand, std::shared_ptr detector_model, std::shared_ptr interactions, siren::dataclasses::SecondaryDistributionRecord & record) const { - std::cout << "in sample physical vertex" << std::endl; + // // std::cout << "in sample physical vertex" << std::endl; siren::math::Vector3D pos = record.initial_position; siren::math::Vector3D dir = record.direction; - // std::cout << "in sample physical vertex-1" << std::endl; + // // std::cout << "in sample physical vertex-1" << std::endl; siren::math::Vector3D endcap_0 = pos; // treat hadronizations differntely if (interactions->HasHadronizations()) { - std::cout << "in sample physical vertex-hadron" << std::endl; + // std::cout << "in sample physical vertex-hadron" << std::endl; record.SetLength(0); return; } - // std::cout << "in sample physical vertex-shouldnm't be here" << std::endl; + // // std::cout << "in sample physical vertex-shouldnm't be here" << std::endl; siren::detector::Path path(detector_model, DetectorPosition(endcap_0), DetectorDirection(dir), std::numeric_limits::infinity()); @@ -86,7 +86,7 @@ void SecondaryPhysicalVertexDistribution::SampleVertex(std::shared_ptr detector_model, std::shared_ptr interactions, siren::dataclasses::InteractionRecord const & record) const { - std::cout << "in sample physical vertex gen prob" << std::endl; + // std::cout << "in sample physical vertex gen prob" << std::endl; siren::math::Vector3D dir(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]); dir.normalize(); diff --git a/projects/distributions/private/secondary/vertex/SecondaryVertexPositionDistribution.cxx b/projects/distributions/private/secondary/vertex/SecondaryVertexPositionDistribution.cxx index 18e9c0b69..70af06641 100644 --- a/projects/distributions/private/secondary/vertex/SecondaryVertexPositionDistribution.cxx +++ b/projects/distributions/private/secondary/vertex/SecondaryVertexPositionDistribution.cxx @@ -15,7 +15,7 @@ namespace distributions { // class SecondaryVertexPositionDistribution : InjectionDistribution //--------------- void SecondaryVertexPositionDistribution::Sample(std::shared_ptr rand, std::shared_ptr detector_model, std::shared_ptr interactions, siren::dataclasses::SecondaryDistributionRecord & record) const { - // std::cout << "sampling vertex" << std::endl; + // // // // // std::cout << "sampling vertex" << std::endl; SampleVertex(rand, detector_model, interactions, record); } diff --git a/projects/injection/private/Injector.cxx b/projects/injection/private/Injector.cxx index 1725b3f64..40069d4ab 100644 --- a/projects/injection/private/Injector.cxx +++ b/projects/injection/private/Injector.cxx @@ -188,7 +188,7 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record double fake_prob; // if contains hadronization, then perform only hadronization if (interactions->HasHadronizations()) { - std::cout << "saw hadronization" << std::endl; + // std::cout << "saw hadronization" << std::endl; double total_frag_prob = 0; std::vector frag_probs; for(auto const & hadronization : interactions->GetHadronizations() ) { @@ -209,7 +209,7 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record } } - std::cout << "Hadronization finished signatures" << std::endl; + // std::cout << "Hadronization finished signatures" << std::endl; // now choose the specific charmed hadron to fragment into @@ -225,25 +225,33 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record matching_hadronizations[index]->SampleFinalState(xsec_record, random); xsec_record.Finalize(record); - std::cout << "hadronization done!" << std::endl; + // std::cout << "hadronization done!" << std::endl; } else { if (interactions->HasCrossSections()) { - std::cout << "saw xsec" << std::endl; + // std::cout << "saw xsec" << std::endl; for(auto const target : available_targets) { if(possible_targets.find(target) != possible_targets.end()) { + // std::cout << "saw xsec: in first for loop" << std::endl; // Get target density double target_density = detector_model->GetParticleDensity(intersections, DetectorPosition(interaction_vertex), target); // Loop over cross sections that have this target std::vector> const & target_cross_sections = interactions->GetCrossSectionsForTarget(target); for(auto const & cross_section : target_cross_sections) { + // std::cout << "saw xsec: in second for loop" << std::endl; // Loop over cross section signatures with the same target std::vector signatures = cross_section->GetPossibleSignaturesFromParents(record.signature.primary_type, target); for(auto const & signature : signatures) { + // std::cout << "saw xsec: in third for loop" << std::endl; + fake_record.signature = signature; fake_record.target_mass = detector_model->GetTargetMass(target); // Add total cross section times density to the total prob + // std::cout << "about to sample total xsec" << std::endl; + fake_prob = target_density * cross_section->TotalCrossSection(fake_record); + // std::cout << "finished sampling total xsec" << std::endl; + total_prob += fake_prob; xsec_prob += fake_prob; // Add total prob to probs @@ -258,7 +266,7 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record } } if (interactions->HasDecays()) { - std::cout << "saw decay" << std::endl; + // std::cout << "saw decay" << std::endl; for(auto const & decay : interactions->GetDecays() ) { for(auto const & signature : decay->GetPossibleSignaturesFromParent(record.signature.primary_type)) { fake_record.signature = signature; @@ -274,7 +282,7 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record } } } - + // std::cout << "continuing...." << std::endl; if(total_prob == 0) throw(siren::utilities::InjectionFailure("No valid interactions for this event!")); // Throw a random number @@ -285,6 +293,7 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record record.signature.target_type = matching_targets[index]; record.signature = matching_signatures[index]; double selected_prob = 0.0; + // std::cout << "finished initializing stuff" << std::endl; for(unsigned int i=0; i 0 ? probs[i] - probs[i - 1] : probs[i]); @@ -294,9 +303,14 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record throw(siren::utilities::InjectionFailure("No valid interactions for this event!")); record.target_mass = detector_model->GetTargetMass(record.signature.target_type); siren::dataclasses::CrossSectionDistributionRecord xsec_record(record); + // std::cout << "finished sampling from primary process" << std::endl; if(r <= xsec_prob) { + // std::cout << "about to sample primary process final state" << std::endl; + matching_cross_sections[index]->SampleFinalState(xsec_record, random); } else { + // std::cout << "about to sample primary process final state" << std::endl; + matching_decays[index - matching_cross_sections.size()]->SampleFinalState(xsec_record, random); } xsec_record.Finalize(record); @@ -307,30 +321,41 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record // // Modifies an InteractionRecord with the new event siren::dataclasses::InteractionRecord Injector::SampleSecondaryProcess(siren::dataclasses::SecondaryDistributionRecord & secondary_record) const { - std::cout << "sampling secondary" << std::endl; - std::cout << "secondary record type is " << secondary_record.type << " " << secondary_record.id << std::endl; + // std::cout << "sampling secondary" << std::endl; + // std::cout << "secondary record type is " << secondary_record.type << " " << secondary_record.id << std::endl; std::shared_ptr secondary_process = secondary_process_map.at(secondary_record.type); std::shared_ptr secondary_interactions = secondary_process->GetInteractions(); std::vector> secondary_distributions = secondary_process->GetSecondaryInjectionDistributions(); - size_t max_tries = 10; + size_t max_tries = 1000; size_t tries = 0; size_t failed_tries = 0; while(true) { - // std::cout << "gotcha" << std::endl; + // // std::cout << "gotcha" << std::endl; + // for(auto & distribution : secondary_distributions) { + // // std::cout << "sample distribution" << std::endl; + // distribution->Sample(random, detector_model, secondary_process->GetInteractions(), secondary_record); + // } + // siren::dataclasses::InteractionRecord record; + // secondary_record.Finalize(record); + // // // std::cout << "sample distribution" << std::endl; + + // SampleCrossSection(record, secondary_interactions); + // return record; + try { for(auto & distribution : secondary_distributions) { - std::cout << "sample distribution" << std::endl; + // std::cout << "sample distribution" << std::endl; distribution->Sample(random, detector_model, secondary_process->GetInteractions(), secondary_record); } siren::dataclasses::InteractionRecord record; secondary_record.Finalize(record); - // std::cout << "sample distribution" << std::endl; + // // std::cout << "sample distribution" << std::endl; SampleCrossSection(record, secondary_interactions); return record; } catch(siren::utilities::InjectionFailure const & e) { - std::cout << "caught error" << std::endl; + // std::cout << "caught error" << std::endl; failed_tries += 1; if(failed_tries > max_tries) { @@ -362,18 +387,19 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { distribution->Sample(random, detector_model, primary_process->GetInteractions(), primary_record); } primary_record.Finalize(record); + // std::cout << "primary record fixed" << std:: endl; SampleCrossSection(record); break; } catch(siren::utilities::InjectionFailure const & e) { failed_tries += 1; - if(tries > max_tries) { + if(failed_tries > max_tries) { throw(siren::utilities::InjectionFailure("Failed to generate primary process!")); break; } continue; } if(tries > max_tries) { - throw(siren::utilities::InjectionFailure("Failed to generate primary process!")); + throw(siren::utilities::InjectionFailure("Failed to generate primary process!!")); break; } } @@ -381,12 +407,12 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { std::shared_ptr parent = tree.add_entry(record); // Secondary Processes - // std::cout << "Sampling primary interactions" << std::endl; + // std::cout << "Sampling primary interactions 2" << std::endl; std::deque, std::shared_ptr>> secondaries; std::function)> add_secondaries = [&](std::shared_ptr parent) { for(size_t i=0; irecord.signature.secondary_types.size(); ++i) { - // std::cout << "for loop 1" << std::endl; + // // std::cout << "for loop 1" << std::endl; siren::dataclasses::ParticleType const & type = parent->record.signature.secondary_types[i]; std::map>::iterator it = secondary_process_map.find(type); if(it == secondary_process_map.end()) { @@ -405,24 +431,67 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { add_secondaries(parent); // std::cout << "num secondaries: " << secondaries.size() << std::endl; while(secondaries.size() > 0) { - // std::cout << "while loop 1" << std::endl; + // // std::cout << "while loop 1" << std::endl; for(int i = secondaries.size() - 1; i >= 0; --i) { - // std::cout << "for loop 2" << std::endl; + // // std::cout << "for loop 2" << std::endl; std::shared_ptr parent = std::get<0>(secondaries[i]); std::shared_ptr secondary_dist = std::get<1>(secondaries[i]); - // std::cout << "for loop 2-1" << std::endl; + // // std::cout << "for loop 2-1" << std::endl; secondaries.erase(secondaries.begin() + i); - siren::dataclasses::InteractionRecord secondary_record = SampleSecondaryProcess(*secondary_dist); - std::shared_ptr secondary_datum = tree.add_entry(secondary_record, parent); - // std::cout << "for loop 2-2" << std::endl; + // std::cout << "Primary Type: " << secondary_dist->record.signature.primary_type << std::endl; + // std::cout << "Secondary Types: "; + // for (const auto& type : secondary_dist->record.signature.secondary_types) { + // std::cout << type << " "; + // } + // std::cout << std::endl; - add_secondaries(secondary_datum); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // this try-catch clock is to debug the secondary process LE no available interaction error + try{ + siren::dataclasses::InteractionRecord secondary_record = SampleSecondaryProcess(*secondary_dist); + std::shared_ptr secondary_datum = tree.add_entry(secondary_record, parent); + add_secondaries(secondary_datum); + } catch (const std::exception& e) { + std::cerr << "Error occurred: " << e.what() << std::endl; + + // Print the primary type and secondary types for debugging + std::cerr << "Primary Type: " << secondary_dist->record.signature.primary_type << std::endl; + std::cerr << "Secondary Types: "; + for (const auto& type : secondary_dist->record.signature.secondary_types) { + std::cerr << type << " "; + } + std::cerr << std::endl; + + // Print the primary momentum + std::cerr << "Primary Momentum: "; + for (double component : secondary_dist->record.primary_momentum) { + std::cerr << component << " "; + } + std::cerr << std::endl; + + // Print the secondary IDs + std::cerr << "Secondary IDs: "; + for (const auto& id : secondary_dist->record.secondary_ids) { + std::cerr << id << " "; + } + std::cerr << std::endl; + throw; + } catch (...) { + std::cerr << "Unknown exception caught!" << std::endl; + throw; + } + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + + } - // std::cout << "while loop 1-2" << std::endl; + // // std::cout << "while loop 1-2" << std::endl; } injected_events += 1; diff --git a/projects/interactions/private/CharmDISFromSpline.cxx b/projects/interactions/private/CharmDISFromSpline.cxx index 45f15b98e..d24740568 100644 --- a/projects/interactions/private/CharmDISFromSpline.cxx +++ b/projects/interactions/private/CharmDISFromSpline.cxx @@ -49,7 +49,17 @@ bool kinematicallyAllowed(double x, double y, double E, double M, double m) { double term = 1 - ((m * m) / (2 * M * E * x)); //the numerator of b (or b*d) double bd = sqrt(term * term - ((m * m) / (E * E))); - return (ad - bd) <= d * y and d * y <= (ad + bd); //Eq. 7 + + // also try the D-Meson Mass? + double s = 2 * M * E; + double Q2 = s * x * y; + double Mc = siren::utilities::Constants::D0Mass; + // if ((Q2 / (1 - x) + pow(M, 2) < pow(M + Mc, 2))) { + // std::cout << "SIREN D Meson constraint is trigged!" << std::endl; + // } + return ((ad - bd) <= d * y and d * y <= (ad + bd)) && (Q2 / (1 - x) + pow(M, 2) >= pow(M + Mc, 2)); //Eq. 7 + // return ((ad - bd) <= d * y and d * y <= (ad + bd)); //Eq. 7 + } } @@ -187,6 +197,8 @@ void CharmDISFromSpline::ReadParamsFromSplineTable() { // returns true if successfully read minimum Q2 bool q2_good = differential_cross_section_.read_key("Q2MIN", minimum_Q2_); + // std::cout << "reading results: " << mass_good << " " << int_good << " " << q2_good << std::endl; + if(!int_good) { // assume DIS to preserve compatability with previous versions interaction_type_ = 1; @@ -209,16 +221,20 @@ void CharmDISFromSpline::ReadParamsFromSplineTable() { } } else { + // // std::cout << "mass and int both not good" << std::endl; if(differential_cross_section_.get_ndim() == 3) { - target_mass_ = (siren::dataclasses::isLepton(siren::dataclasses::ParticleType::PPlus)+ - siren::dataclasses::isLepton(siren::dataclasses::ParticleType::Neutron))/2; + // std::cout << "dim is 3" << std::endl; + target_mass_ = siren::utilities::Constants::isoscalarMass; + } else if(differential_cross_section_.get_ndim() == 2) { - target_mass_ = siren::dataclasses::isLepton(siren::dataclasses::ParticleType::EMinus); + target_mass_ = siren::utilities::Constants::electronMass; } else { throw std::runtime_error("Logic error. Spline dimensionality is not 2, or 3!"); } } } + + // std::cout << "target mass is " << target_mass_ << std::endl; } void CharmDISFromSpline::InitializeSignatures() { @@ -277,6 +293,7 @@ double CharmDISFromSpline::TotalCrossSection(dataclasses::InteractionRecord cons rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); double primary_energy; primary_energy = interaction.primary_momentum[0]; + // std::cout << "primary energy is " << primary_energy << std::endl; // if we are below threshold, return 0 if(primary_energy < InteractionThreshold(interaction)) return 0; @@ -287,6 +304,7 @@ double CharmDISFromSpline::TotalCrossSection(siren::dataclasses::ParticleType pr if(not primary_types_.count(primary_type)) { throw std::runtime_error("Supplied primary not supported by cross section!"); } + // std::cout << "now in real sample total xsec func" << std::endl; double log_energy = log10(primary_energy); if(log_energy < total_cross_section_.lower_extent(0) @@ -298,7 +316,10 @@ double CharmDISFromSpline::TotalCrossSection(siren::dataclasses::ParticleType pr } int center; + // std::cout << "maybe problem is here?" << std::endl; total_cross_section_.searchcenters(&log_energy, ¢er); + // std::cout << "maybe problem is here??" << std::endl; + double log_xs = total_cross_section_.ndsplineeval(&log_energy, ¢er, 0); return unit * std::pow(10.0, log_xs); @@ -323,12 +344,18 @@ double CharmDISFromSpline::DifferentialCrossSection(dataclasses::InteractionReco double Q2 = -q.dot(q); double y = 1.0 - p2.dot(p3) / p2.dot(p1); double x = Q2 / (2.0 * p2.dot(q)); + // apply slow scaling here + // double slow_scale = 1 + pow(siren::utilities::Constants::CharmMass, 2) / pow(Q2, 2); + // double xi = x * slow_scale; double lepton_mass = GetLeptonMass(interaction.signature.secondary_types[lepton_index]); + // return DifferentialCrossSection(primary_energy, xi, y, lepton_mass, Q2); return DifferentialCrossSection(primary_energy, x, y, lepton_mass, Q2); + } double CharmDISFromSpline::DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2) const { + bool check_criteria = false; // this is used to gauge kinematic constraint in xsec and SIREN impementations, will eventually be deleted double log_energy = log10(energy); // check preconditions if(log_energy < differential_cross_section_.lower_extent(0) @@ -348,10 +375,11 @@ double CharmDISFromSpline::DifferentialCrossSection(double energy, double x, dou if(Q2 < minimum_Q2_) // cross section not calculated, assumed to be zero return 0; - // cross section should be zero, but this check is missing from the original - // CSMS calculation, so we must add it here - if(!kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) - return 0; + if (!check_criteria) { + if(!kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) { + return 0; + } + } std::array coordinates{{log_energy, log10(x), log10(y)}}; std::array centers; @@ -359,6 +387,33 @@ double CharmDISFromSpline::DifferentialCrossSection(double energy, double x, dou return 0; double result = pow(10., differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0)); assert(result >= 0); + + if (check_criteria) { + // this is a check of kinematic constraint implementation + if (result == 0) { + if(kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) { + std::cout << "xsec gives 0 but kinematically allowed!" << std::endl; + } + } + + if(!kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) { + // check if this is due to charm production constraint + double M = target_mass_; + double E = energy; + double s = 2 * M * E; + double Q2 = s * x * y; + double Mc = siren::utilities::Constants::D0Mass; + if ((Q2 / (1 - x) + pow(M, 2) < pow(M + Mc, 2))) { // if so check result + if (result != 0) { + std::cout << "SIREN constraint not passed but xsec does not give 0!" << std::endl; + } + } + return 0; + } + } + + + return unit * result; } @@ -373,7 +428,7 @@ void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR if (differential_cross_section_.get_ndim() != 3) { throw std::runtime_error("I expected 3 dimensions in the cross section spline, but got " + std::to_string(differential_cross_section_.get_ndim()) +". Maybe your fits file doesn't have the right 'INTERACTION' key?"); } - + // std::cout << "in sample final state of charm DIS from spline" << std::endl; rk::P4 p1(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); rk::P4 p2(geom3::Vector3(0, 0, 0), record.target_mass); @@ -395,10 +450,11 @@ void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR double m = GetLeptonMass(record.signature.secondary_types[lepton_index]); double m1 = record.primary_mass; + // std::cout << "getting mass of primary: " << m1 << std::endl; double m3 = m; double E1_lab = p1_lab.e(); double E2_lab = p2_lab.e(); - + // std::cout << "getting energy of primary: " << E1_lab << std::endl; // The out-going particle always gets at least enough energy for its rest mass double yMax = 1 - m / primary_energy; @@ -437,10 +493,17 @@ void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR do { // rejection sample a point which is kinematically allowed by calculation limits double trialQ; + double trials = 0; + // std::cout << "do loop 1" << std::endl; do { + // std::cout << "do loop 2" << std::endl; + if (trials >= 100) throw std::runtime_error("too much trials"); + trials += 1; kin_vars[1] = random->Uniform(logXMin,0); kin_vars[2] = random->Uniform(logYMin,logYMax); trialQ = (2 * E1_lab * E2_lab) * pow(10., kin_vars[1] + kin_vars[2]); + // std::cout << kin_vars[1] << " " << kin_vars[2] << " " << trialQ << " " << minimum_Q2_ << std::endl; + // std::cout << primary_energy << " " << target_mass_ << " " << m << std::endl; } while(trialQ(Py_None); @@ -37,6 +41,72 @@ bool CharmHadronization::equal(Hadronization const & other) const { return primary_types == x->primary_types; } +void CharmHadronization::normalize_pdf() { + if (fragmentation_integral == 0){ + std::function integrand = [&] (double x) -> double { + return (0.8 / x ) / (std::pow(1 - (1 / x) - (0.2 / (1 - x)), 2)); + }; + fragmentation_integral = siren::utilities::rombergIntegrate(integrand, 0.001, 0.999); + } else { + std::cout << "Something is wrong... you already computed the normalization" << std::endl; + return; + } +} + +double CharmHadronization::sample_pdf(double x) const { + return (0.8 / x ) / (std::pow(1 - (1 / x) - (0.2 / (1 - x)), 2)) / fragmentation_integral; +} + +void CharmHadronization::compute_cdf() { + // first set the z nodes + std::vector zspline; + for (int i = 0; i < 100; ++i) { + zspline.push_back(0.01 + i * (0.99-0.01) / 100 ); + } + + // declare the cdf vectors + std::vector cdf_vector; + std::vector cdf_z_nodes; + std::vector pdf_vector; + + cdf_z_nodes.push_back(0); + cdf_vector.push_back(0); + pdf_vector.push_back(0); + + // compute the spline table + for (int i = 0; i < zspline.size(); ++i) { + if (i == 0) { + double cur_z = zspline[i]; + double cur_pdf = sample_pdf(cur_z); + double area = cur_z * cur_pdf * 0.5; + pdf_vector.push_back(cur_pdf); + cdf_vector.push_back(area); + cdf_z_nodes.push_back(cur_z); + continue; + } + double cur_z = zspline[i]; + double cur_pdf = sample_pdf(cur_z); + double area = 0.5 * (pdf_vector[i - 1] + cur_pdf) * (zspline[i] - zspline[i - 1]); + pdf_vector.push_back(cur_pdf); + cdf_z_nodes.push_back(cur_z); + cdf_vector.push_back(area + cdf_vector.back()); + } + + cdf_z_nodes.push_back(1); + cdf_vector.push_back(1); + pdf_vector.push_back(0); + + + // set the spline table + siren::utilities::TableData1D inverse_cdf_data; + inverse_cdf_data.x = cdf_vector; + inverse_cdf_data.f = cdf_z_nodes; + + inverseCdfTable = siren::utilities::Interpolator1D(inverse_cdf_data); + + return; +} + double CharmHadronization::getHadronMass(siren::dataclasses::ParticleType hadron_type) { switch(hadron_type){ case siren::dataclasses::ParticleType::D0: @@ -96,16 +166,27 @@ void CharmHadronization::SampleFinalState(dataclasses::CrossSectionDistributionR rk::P4 pc(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); double p3c = std::sqrt(std::pow(interaction.primary_momentum[1], 2) + std::pow(interaction.primary_momentum[2], 2) + std::pow(interaction.primary_momentum[3], 2)); double Ec = pc.e(); //energy of primary charm - // std::cout << "hadronization sample final state" << std::endl; - // double peterson_distribution(double z) { - // return 0.8 / x * std::pow((1 - 1 / x - 0.2 / (1 - x)), 2) - // } + double mCH = getHadronMass(interaction.signature.secondary_types[1]); // obtain charmed hadron mass - double z = 0.6; // replace by actual sampling: inverse CDF - double ECH = z * Ec; + bool accept; + double randValue; + double z; + double ECH; + + // sample again if this eenrgy is not kinematically allowed + do { + randValue = random->Uniform(0,1); + z = inverseCdfTable(randValue); + ECH = z * Ec; + if (std::pow(ECH, 2) - std::pow(mCH, 2) <= 0) { + accept = false; + } else { + accept = true; + } + double new_debug = std::pow(ECH, 2) - std::pow(mCH, 2); + } while (!accept); // is it ok to compute everything in lab frame? - double mCH = getHadronMass(interaction.signature.secondary_types[1]); // obtain charmed hadron mass double p3CH = std::sqrt(std::pow(ECH, 2) - std::pow(mCH, 2)); //obtain charmed hadron 3-momentum double rCH = p3CH/p3c; // ratio of momentum carried away by the charmed hadron, assume collinearity rk::P4 p4CH(geom3::Vector3(rCH * interaction.primary_momentum[1], rCH * interaction.primary_momentum[2], rCH * interaction.primary_momentum[3]), mCH); @@ -115,9 +196,6 @@ void CharmHadronization::SampleFinalState(dataclasses::CrossSectionDistributionR double rX = p3X/p3c; // assume collinear rk::P4 p4X(geom3::Vector3(rX * interaction.primary_momentum[1], rX * interaction.primary_momentum[2], rX * interaction.primary_momentum[3]), 0); - - // update interaction parameters: to be added here later - // new implementation of updateing outgoing particles std::vector & secondaries = interaction.GetSecondaryParticleRecords(); siren::dataclasses::SecondaryParticleRecord & hadronic_vertex = secondaries[0]; @@ -131,26 +209,6 @@ void CharmHadronization::SampleFinalState(dataclasses::CrossSectionDistributionR d_meson.SetMass(p4CH.m()); d_meson.SetHelicity(interaction.primary_helicity); - // interaction.secondary_momenta.resize(2); - // interaction.secondary_masses.resize(2); - // interaction.secondary_helicities.resize(2); - - // // the hadronic shower - // interaction.secondary_momenta[0][0] = p4X.e(); - // interaction.secondary_momenta[0][1] = p4X.px(); - // interaction.secondary_momenta[0][2] = p4X.py(); - // interaction.secondary_momenta[0][3] = p4X.pz(); - // interaction.secondary_masses[0] = 0; - // interaction.secondary_helicities[0] = interaction.primary_helicity; // true? - - // // the charmed hadron - // interaction.secondary_momenta[1][0] = p4CH.e(); - // interaction.secondary_momenta[1][1] = p4CH.px(); - // interaction.secondary_momenta[1][2] = p4CH.py(); - // interaction.secondary_momenta[1][3] = p4CH.pz(); - // interaction.secondary_masses[1] = p4CH.m(); - // interaction.secondary_helicities[1] = interaction.primary_helicity; // true? - } double CharmHadronization::FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const { diff --git a/projects/interactions/public/SIREN/interactions/CharmHadronization.h b/projects/interactions/public/SIREN/interactions/CharmHadronization.h index 86b7cc839..e81cfa4bb 100644 --- a/projects/interactions/public/SIREN/interactions/CharmHadronization.h +++ b/projects/interactions/public/SIREN/interactions/CharmHadronization.h @@ -22,6 +22,10 @@ #include "SIREN/utilities/Random.h" // for SIREN_random #include "SIREN/geometry/Geometry.h" #include "SIREN/utilities/Constants.h" // for electronMass +#include "SIREN/utilities/Interpolator.h" +#include "SIREN/utilities/Integration.h" + + namespace siren { namespace dataclasses { class InteractionRecord; } } @@ -35,7 +39,11 @@ class CharmHadronization : public Hadronization { friend cereal::access; private: const std::set primary_types = {siren::dataclasses::Particle::ParticleType::Charm, siren::dataclasses::Particle::ParticleType::CharmBar}; - + // z pdf setting should be enabled in the future, for now we hard code the Peterson function + double fragmentation_integral = 0; // for storing the integrated unnormed pdf + void normalize_pdf(); // for normalizing pdf and stroing integral, to be called at initialization + + siren::utilities::Interpolator1D inverseCdfTable; public: CharmHadronization(); @@ -51,6 +59,8 @@ friend cereal::access; double FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const override; + double sample_pdf(double z) const; + void compute_cdf(); static double getHadronMass(siren::dataclasses::ParticleType hadron_type); public: @@ -77,6 +87,7 @@ friend cereal::access; } // namespace siren CEREAL_CLASS_VERSION(siren::interactions::CharmHadronization, 0); - +CEREAL_REGISTER_TYPE(siren::interactions::CharmHadronization); +CEREAL_REGISTER_POLYMORPHIC_RELATION(siren::interactions::Hadronization, siren::interactions::CharmHadronization); #endif // SIREN_CharmHadronization_H diff --git a/python/SIREN_Controller.py b/python/SIREN_Controller.py index df7854cd4..f754a4598 100644 --- a/python/SIREN_Controller.py +++ b/python/SIREN_Controller.py @@ -580,9 +580,16 @@ def SaveEvents(self, filename, fill_tables_at_exit=True, datasets["num_interactions"].append(id+1) # save injector and weighter - # self.injector.SaveInjector(filename) + self.injector.SaveInjector(filename) # weighter saving not yet supported - #self.weighter.SaveWeighter(filename) + self.weighter.SaveWeighter(filename) + + # Add print statements to check the lengths of all datasets + # for key, value in datasets.items(): + # print(f"Length of {key}: {len(value)}") + # if isinstance(value[0], list): # If it's a list of lists, check the inner lengths + # for idx, sublist in enumerate(value): + # print(f" Length of {key}[{idx}]: {len(sublist)}") # save events ak_array = ak.Array(datasets) diff --git a/resources/Examples/DMesonExample/DIS_D.py b/resources/Examples/DMesonExample/DIS_D.py index 3114111ca..8fb483a9e 100644 --- a/resources/Examples/DMesonExample/DIS_D.py +++ b/resources/Examples/DMesonExample/DIS_D.py @@ -4,7 +4,7 @@ from siren.SIREN_Controller import SIREN_Controller # Number of events to inject -events_to_inject = 50000 +events_to_inject = 5 # Expeirment to run experiment = "IceCube" diff --git a/resources/Examples/DMesonExample/make_plots.py b/resources/Examples/DMesonExample/make_plots.py index c8b246aa9..52d4dad98 100644 --- a/resources/Examples/DMesonExample/make_plots.py +++ b/resources/Examples/DMesonExample/make_plots.py @@ -4,9 +4,18 @@ from matplotlib.colors import LogNorm from mpl_toolkits.axes_grid1 import make_axes_locatable from parse_output import analysis -filename = "output/FullSim.parquet" -plt.style.use('paper.mplstyle') +import os + +pathname = "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/miaochenjin/DBSearch/SIREN_outputs/" +# parquetname = "0708_test/0708_test_.parquet" +expname = "0709_astro_flux" +parquetname = "{}/{}_.parquet".format(expname, expname) + +filename = os.path.join(pathname, parquetname) +savedir = os.path.join(pathname, "plots/") + +plt.style.use('paper.mplstyle') sim = analysis(filename) @@ -43,8 +52,8 @@ def xsec(E): def analytic_free_path(E): return (m_ice / (rho * N * xsec(E))) / 100 # convert to m -def plot_separation_distribution(analysis_): - D0_energies, D0_separations, Dp_energies, Dp_separations = analysis_.separation_analysis() +def plot_separation_distribution(analysis_, dim = 2): + D0_energies, D0_separations, Dp_energies, Dp_separations, D0_weights, Dp_weights = analysis_.separation_analysis() min_eng = 1e1 max_eng = 1e9 energy_bins = np.logspace(np.log10(min_eng), np.log10(max_eng), 20) @@ -58,54 +67,79 @@ def plot_separation_distribution(analysis_): min_sep = 1e-3 max_sep = 50000 log_separation_bins = np.logspace(np.log10(min_sep), np.log10(max_sep), 20) + sep_bin_widths = np.sqrt(log_separation_bins[1:] * log_separation_bins[:-1]) - X2, Y2 = np.meshgrid(energy_bins, log_separation_bins) - log_hist_D0, _, _ = np.histogram2d(D0_energies, D0_separations, bins = (energy_bins, log_separation_bins)) - log_hist_Dp, _, _ = np.histogram2d(Dp_energies, Dp_separations, bins = (energy_bins, log_separation_bins)) - log_hist_D0 = normalize(log_hist_D0, energy_bins, log_separation_bins) - log_hist_Dp = normalize(log_hist_Dp, energy_bins, log_separation_bins) + if dim == 2: - fig, axes = plt.subplots(nrows = 1, ncols = 2, figsize = (11, 5)) + X2, Y2 = np.meshgrid(energy_bins, log_separation_bins) + log_hist_D0, _, _ = np.histogram2d(D0_energies, D0_separations, bins = (energy_bins, log_separation_bins), weights = D0_weights) + log_hist_Dp, _, _ = np.histogram2d(Dp_energies, Dp_separations, bins = (energy_bins, log_separation_bins), weights = Dp_weights) + log_hist_D0 = normalize(log_hist_D0, energy_bins, log_separation_bins) + log_hist_Dp = normalize(log_hist_Dp, energy_bins, log_separation_bins) - log_im1 = axes[0].pcolor(X2, Y2, log_hist_D0.T, cmap="plasma", alpha = 0.7, vmin=0, vmax=1) - log_im2 = axes[1].pcolor(X2, Y2, log_hist_Dp.T, cmap="plasma", alpha = 0.7, vmin=0, vmax=1) + fig, axes = plt.subplots(nrows = 1, ncols = 2, figsize = (11, 5)) - # divider1 = make_axes_locatable(axes[0]) - # cax1 = divider1.append_axes('right', size='5%', pad=0.05) - divider2 = make_axes_locatable(axes[1]) - cax2 = divider2.append_axes('right', size='5%', pad=0.05) - fig.colorbar(log_im2, cax=cax2, orientation='vertical', alpha = 0.7) - # fig.colorbar(log_im1, cax=cax1, orientation='vertical', alpha = 0.7) + log_im1 = axes[0].pcolor(X2, Y2, log_hist_D0.T, cmap="plasma", alpha = 0.7, vmin=0, vmax=1) + log_im2 = axes[1].pcolor(X2, Y2, log_hist_Dp.T, cmap="plasma", alpha = 0.7, vmin=0, vmax=1) - axes[0].set_title(r"$D^0$ Separation") - axes[1].set_title(r"$D^+$ Separation") + # divider1 = make_axes_locatable(axes[0]) + # cax1 = divider1.append_axes('right', size='5%', pad=0.05) + divider2 = make_axes_locatable(axes[1]) + cax2 = divider2.append_axes('right', size='5%', pad=0.05) + fig.colorbar(log_im2, cax=cax2, orientation='vertical', alpha = 0.7) + # fig.colorbar(log_im1, cax=cax1, orientation='vertical', alpha = 0.7) - axes[0].set_xlabel(r"$E_{D^0}$ [GeV]") - axes[1].set_xlabel(r"$E_{D^+}$ [GeV]") + axes[0].set_title(r"$D^0$ Separation") + axes[1].set_title(r"$D^+$ Separation") - axes[0].set_ylabel("Separation Length [m]") + axes[0].set_xlabel(r"$E_{D^0}$ [GeV]") + axes[1].set_xlabel(r"$E_{D^+}$ [GeV]") - axes[0].set_xscale('log') - axes[1].set_xscale('log') - axes[0].set_yscale('log') - axes[1].set_yscale('log') + axes[0].set_ylabel("Separation Length [m]") - axes[0].set_ylim(min_sep, max_sep) - axes[1].set_ylim(min_sep, max_sep) - axes[0].set_xlim(min_eng, max_eng) - axes[1].set_xlim(min_eng, max_eng) + axes[0].set_xscale('log') + axes[1].set_xscale('log') + axes[0].set_yscale('log') + axes[1].set_yscale('log') - # also plot the analytic lines - axes[0].plot(energy_bins_centers, D0_analytic_lengths, color = '#FEF3E8', alpha = 0.7) - axes[1].plot(energy_bins_centers, Dp_analytic_lengths, label = r"$d = \frac{E \tau}{mc}$", color = '#FEF3E8', alpha = 0.7) + axes[0].set_ylim(min_sep, max_sep) + axes[1].set_ylim(min_sep, max_sep) + axes[0].set_xlim(min_eng, max_eng) + axes[1].set_xlim(min_eng, max_eng) - legend = axes[1].legend(loc = 'upper left') - for text in legend.get_texts(): - text.set_color('#FEF3E8') + # also plot the analytic lines + axes[0].plot(energy_bins_centers, D0_analytic_lengths, color = '#FEF3E8', alpha = 0.7) + axes[1].plot(energy_bins_centers, Dp_analytic_lengths, label = r"$d = \frac{E \tau}{mc}$", color = '#FEF3E8', alpha = 0.7) + + legend = axes[1].legend(loc = 'upper left') + for text in legend.get_texts(): + text.set_color('#FEF3E8') + + savename = os.path.join(savedir, "Separation_Length_Distribution") + + elif dim == 1: + + D0_separations.extend(Dp_weights) + D0_weights.extend(Dp_weights) + + sep_hist, _ = np.histogram(D0_separations, log_separation_bins, weights = D0_weights) + fig, ax = plt.subplots(1, 1, figsize = (8, 6)) + ax.hist(log_separation_bins[:-1], bins = log_separation_bins, weights = sep_hist / sep_bin_widths, \ + label = r"$\phi \sim E^{-2}$", \ + alpha = 0.9, color = '#D06C9D', histtype = 'step') + + savename = os.path.join(savedir, "PowerLaw_2_Separation_Length_Distribution") + ax.legend(loc = 'upper right') + ax.set_xscale('log') + ax.set_yscale('log') + ax.set_xlabel(r'$d_{\textrm{Sep}}$ [m]') + ax.set_ylabel('Normalized Weights Event Count') + + fig.savefig(savename, bbox_inches = 'tight') - fig.savefig("./plots/Separation_Length_Distribution", bbox_inches = 'tight') +plot_separation_distribution(sim, dim = 1) +plot_separation_distribution(sim, dim = 2) -# plot_separation_distribution(sim) def plot_2d_energy_loss(analysis_): E_D0, E_Dp, n_D0, n_Dp = analysis_.energy_loss_analysis_2d() @@ -149,8 +183,8 @@ def plot_2d_energy_loss(analysis_): fig.savefig("./plots/Energy_loss_2d_Distribution", bbox_inches = 'tight') -plot_2d_energy_loss(sim) -exit(0) +# plot_2d_energy_loss(sim) +# exit(0) def plot_free_path_distribution(): D0_E_list, D0_free_path_list, Dp_E_list, Dp_free_path_list = analysis_.free_path_analysis() @@ -213,4 +247,4 @@ def plot_free_path_distribution(): fig.savefig("./plots/Free_Path_Distribution", bbox_inches = 'tight') return -plot_free_path_distribution() \ No newline at end of file +# plot_free_path_distribution() \ No newline at end of file diff --git a/resources/Examples/DMesonExample/parse_output.py b/resources/Examples/DMesonExample/parse_output.py index 32a316953..c7a79fde5 100644 --- a/resources/Examples/DMesonExample/parse_output.py +++ b/resources/Examples/DMesonExample/parse_output.py @@ -62,6 +62,8 @@ def separation_analysis(self): D0_separations = [] Dp_energies = [] Dp_separations = [] + D0_weights = [] + Dp_weights = [] for i in range(self.num_events): cur_event = event(self.df.iloc[i]) print("{}/{}".format(i, self.num_events), end = '\r') @@ -70,17 +72,23 @@ def separation_analysis(self): # extract the vertex separations D0_separations.append(cur_event.D_DECAY_LEN()) D0_energies.append(cur_event.row["primary_momentum"][2][0]) + D0_weights.append(cur_event.row["event_weight"]) + elif cur_event.DTYPE() == 411: # This is D+ # extract the vertex separations Dp_separations.append(cur_event.D_DECAY_LEN()) Dp_energies.append(cur_event.row["primary_momentum"][2][0]) - return D0_energies, D0_separations, Dp_energies, Dp_separations + Dp_weights.append(cur_event.row["event_weight"]) + + return D0_energies, D0_separations, Dp_energies, Dp_separations, D0_weights, Dp_weights def energy_loss_analysis_2d(self): E_D0 = [] E_Dp = [] n_D0 = [] n_Dp = [] + w_D0 = [] + w_Dp = [] for i in range(self.num_events): cur_event = event(self.df.iloc[i]) print("{}/{}".format(i, self.num_events), end = '\r') @@ -88,9 +96,13 @@ def energy_loss_analysis_2d(self): # extract the vertex separations n_D0.append(cur_event.row["num_interactions"] - 3) E_D0.append(cur_event.row["primary_momentum"][2][0]) + w_D0.append(cur_event.row["event_weight"]) + elif cur_event.DTYPE() == 411: # This is D+ # extract the vertex separations n_Dp.append(cur_event.row["num_interactions"] - 3) E_Dp.append(cur_event.row["primary_momentum"][2][0]) - return E_D0, E_Dp, n_D0, n_Dp + w_Dp.append(cur_event.row["event_weight"]) + + return E_D0, E_Dp, n_D0, n_Dp, w_D0, w_Dp From a6d66aa1c45fbd34eaca149889c608551df047ea Mon Sep 17 00:00:00 2001 From: Nicholas Kamp <43788191+nickkamp1@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:07:59 -0400 Subject: [PATCH 04/93] Update DetectorModel.cxx for PR --- projects/detector/private/DetectorModel.cxx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/projects/detector/private/DetectorModel.cxx b/projects/detector/private/DetectorModel.cxx index e8cbcffb6..75a86dc75 100644 --- a/projects/detector/private/DetectorModel.cxx +++ b/projects/detector/private/DetectorModel.cxx @@ -683,8 +683,7 @@ double DetectorModel::GetInteractionDensity(Geometry::IntersectionList const & i std::vector const & total_cross_sections, double const & total_decay_length) const { Vector3D direction = p0 - intersections.position; - // std::cout << "direction: " << direction.magnitude() << std::endl; - if(direction.magnitude() == 0 || direction.magnitude() <= 1e-05) { + if(direction.magnitude() == 0 || direction.magnitude() <= 1e-6) { direction = intersections.direction; } else { direction.normalize(); @@ -980,25 +979,21 @@ double DetectorModel::GetInteractionDepthInCGS(Geometry::IntersectionList const std::vector const & total_cross_sections, double const & total_decay_length) const { - // std::cout << p0 << " " << p1 << " " << intersections.direction << std::endl; if(p0 == p1) { return 0.0; } Vector3D direction = p1 - p0; double distance = direction.magnitude(); - // std::cout << "distance is " << distance << std::endl; if(distance == 0.0) { return 0.0; } - if(direction.magnitude() <= 1e-05) { - // std::cout << "triggered" << std::endl; - return 0.0; + if(direction.magnitude() <= 1e-6) { + direction = intersections.direction; } direction.normalize(); double dot = intersections.direction * direction; assert(std::abs(1.0 - std::abs(dot)) < 1e-6); - // std::cout << "not at this point" << std::endl; double offset = (intersections.position - p0) * direction; if(dot < 0) { From 4414cbb02a0307b19216117284620c95a71bd60d Mon Sep 17 00:00:00 2001 From: Nicholas Kamp <43788191+nickkamp1@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:18:45 -0400 Subject: [PATCH 05/93] Update Injector.cxx to fixes failed_tries bug --- projects/injection/private/Injector.cxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/injection/private/Injector.cxx b/projects/injection/private/Injector.cxx index 40069d4ab..9bf04c1b5 100644 --- a/projects/injection/private/Injector.cxx +++ b/projects/injection/private/Injector.cxx @@ -331,6 +331,7 @@ siren::dataclasses::InteractionRecord Injector::SampleSecondaryProcess(siren::da size_t tries = 0; size_t failed_tries = 0; while(true) { + tries += 1; // // std::cout << "gotcha" << std::endl; // for(auto & distribution : secondary_distributions) { // // std::cout << "sample distribution" << std::endl; @@ -364,7 +365,7 @@ siren::dataclasses::InteractionRecord Injector::SampleSecondaryProcess(siren::da } continue; } - if(failed_tries > max_tries) { + if(tries > max_tries) { throw(siren::utilities::InjectionFailure("Failed to generate secondary process!")); break; } From 7e21f18d7134bf461911a5b0e236536820907337 Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Thu, 22 Aug 2024 17:46:04 -0400 Subject: [PATCH 06/93] updated file for PR comments, preparing for PR again --- .../SIREN/dataclasses/ParticleTypes.def | 1 + projects/detector/private/DetectorModel.cxx | 5 +- .../SecondaryBoundedVertexDistribution.cxx | 4 - .../SecondaryPhysicalVertexDistribution.cxx | 10 - .../SecondaryVertexPositionDistribution.cxx | 1 - projects/injection/private/Injector.cxx | 108 +------- projects/injection/private/Weighter.cxx | 2 +- .../private/CharmDISFromSpline.cxx | 58 +--- .../private/CharmHadronization.cxx | 2 +- .../interactions/private/CharmMesonDecay.cxx | 97 ------- projects/interactions/private/DMesonELoss.cxx | 50 +--- resources/Examples/DMesonExample/DIS_D.py | 71 +++-- .../Examples/DMesonExample/make_plots.py | 250 ------------------ 13 files changed, 69 insertions(+), 590 deletions(-) delete mode 100644 resources/Examples/DMesonExample/make_plots.py diff --git a/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def b/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def index 73b578f97..af36148c8 100644 --- a/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def +++ b/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def @@ -194,6 +194,7 @@ X(NuclInt, -2000001004) X(MuPair, -2000001005) X(Hadrons, -2000001006) X(Decay, -2000001007) +X(Hadronization, 99) X(ContinuousEnergyLoss, -2000001111) X(FiberLaser, -2000002100) X(N2Laser, -2000002101) diff --git a/projects/detector/private/DetectorModel.cxx b/projects/detector/private/DetectorModel.cxx index 75a86dc75..37cce5212 100644 --- a/projects/detector/private/DetectorModel.cxx +++ b/projects/detector/private/DetectorModel.cxx @@ -683,7 +683,7 @@ double DetectorModel::GetInteractionDensity(Geometry::IntersectionList const & i std::vector const & total_cross_sections, double const & total_decay_length) const { Vector3D direction = p0 - intersections.position; - if(direction.magnitude() == 0 || direction.magnitude() <= 1e-6) { + if(direction.magnitude() == 0 || direction.magnitude() <= 1e-5) { direction = intersections.direction; } else { direction.normalize(); @@ -987,8 +987,9 @@ double DetectorModel::GetInteractionDepthInCGS(Geometry::IntersectionList const if(distance == 0.0) { return 0.0; } - if(direction.magnitude() <= 1e-6) { + if(direction.magnitude() <= 1e-5) { direction = intersections.direction; + // return 1e-05; // I have to do this to ensure that it works } direction.normalize(); diff --git a/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx b/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx index a86fffb1c..43705a3ea 100644 --- a/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx +++ b/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx @@ -53,8 +53,6 @@ double log_one_minus_exp_of_negative(double x) { void SecondaryBoundedVertexDistribution::SampleVertex(std::shared_ptr rand, std::shared_ptr detector_model, std::shared_ptr interactions, siren::dataclasses::SecondaryDistributionRecord & record) const { - // std::cout << "in sample bounded vertex" << std::endl; - siren::math::Vector3D pos = record.initial_position; siren::math::Vector3D dir = record.direction; @@ -123,8 +121,6 @@ void SecondaryBoundedVertexDistribution::SampleVertex(std::shared_ptr detector_model, std::shared_ptr interactions, siren::dataclasses::InteractionRecord const & record) const { - // std::cout << "in sample bounded vertex gen prob" << std::endl; - siren::math::Vector3D dir(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]); dir.normalize(); siren::math::Vector3D vertex(record.interaction_vertex); diff --git a/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx b/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx index d49a42f03..f5d327b7a 100644 --- a/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx +++ b/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx @@ -51,22 +51,15 @@ double log_one_minus_exp_of_negative(double x) { void SecondaryPhysicalVertexDistribution::SampleVertex(std::shared_ptr rand, std::shared_ptr detector_model, std::shared_ptr interactions, siren::dataclasses::SecondaryDistributionRecord & record) const { - // // std::cout << "in sample physical vertex" << std::endl; siren::math::Vector3D pos = record.initial_position; siren::math::Vector3D dir = record.direction; - // // std::cout << "in sample physical vertex-1" << std::endl; siren::math::Vector3D endcap_0 = pos; // treat hadronizations differntely if (interactions->HasHadronizations()) { - // std::cout << "in sample physical vertex-hadron" << std::endl; - record.SetLength(0); return; } - // // std::cout << "in sample physical vertex-shouldnm't be here" << std::endl; - - siren::detector::Path path(detector_model, DetectorPosition(endcap_0), DetectorDirection(dir), std::numeric_limits::infinity()); path.ClipToOuterBounds(); @@ -86,7 +79,6 @@ void SecondaryPhysicalVertexDistribution::SampleVertex(std::shared_ptr detector_model, std::shared_ptr interactions, siren::dataclasses::InteractionRecord const & record) const { - // std::cout << "in sample physical vertex gen prob" << std::endl; - siren::math::Vector3D dir(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]); dir.normalize(); siren::math::Vector3D vertex(record.interaction_vertex); diff --git a/projects/distributions/private/secondary/vertex/SecondaryVertexPositionDistribution.cxx b/projects/distributions/private/secondary/vertex/SecondaryVertexPositionDistribution.cxx index 70af06641..87b44e63d 100644 --- a/projects/distributions/private/secondary/vertex/SecondaryVertexPositionDistribution.cxx +++ b/projects/distributions/private/secondary/vertex/SecondaryVertexPositionDistribution.cxx @@ -15,7 +15,6 @@ namespace distributions { // class SecondaryVertexPositionDistribution : InjectionDistribution //--------------- void SecondaryVertexPositionDistribution::Sample(std::shared_ptr rand, std::shared_ptr detector_model, std::shared_ptr interactions, siren::dataclasses::SecondaryDistributionRecord & record) const { - // // // // // std::cout << "sampling vertex" << std::endl; SampleVertex(rand, detector_model, interactions, record); } diff --git a/projects/injection/private/Injector.cxx b/projects/injection/private/Injector.cxx index 9bf04c1b5..a77338cff 100644 --- a/projects/injection/private/Injector.cxx +++ b/projects/injection/private/Injector.cxx @@ -188,7 +188,6 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record double fake_prob; // if contains hadronization, then perform only hadronization if (interactions->HasHadronizations()) { - // std::cout << "saw hadronization" << std::endl; double total_frag_prob = 0; std::vector frag_probs; for(auto const & hadronization : interactions->GetHadronizations() ) { @@ -209,9 +208,6 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record } } - // std::cout << "Hadronization finished signatures" << std::endl; - - // now choose the specific charmed hadron to fragment into double r = random->Uniform(0, total_frag_prob); unsigned int index = 0; @@ -224,34 +220,22 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record matching_hadronizations[index]->SampleFinalState(xsec_record, random); xsec_record.Finalize(record); - - // std::cout << "hadronization done!" << std::endl; - } else { if (interactions->HasCrossSections()) { - // std::cout << "saw xsec" << std::endl; for(auto const target : available_targets) { if(possible_targets.find(target) != possible_targets.end()) { - // std::cout << "saw xsec: in first for loop" << std::endl; // Get target density double target_density = detector_model->GetParticleDensity(intersections, DetectorPosition(interaction_vertex), target); // Loop over cross sections that have this target std::vector> const & target_cross_sections = interactions->GetCrossSectionsForTarget(target); for(auto const & cross_section : target_cross_sections) { - // std::cout << "saw xsec: in second for loop" << std::endl; // Loop over cross section signatures with the same target std::vector signatures = cross_section->GetPossibleSignaturesFromParents(record.signature.primary_type, target); for(auto const & signature : signatures) { - // std::cout << "saw xsec: in third for loop" << std::endl; - fake_record.signature = signature; fake_record.target_mass = detector_model->GetTargetMass(target); // Add total cross section times density to the total prob - // std::cout << "about to sample total xsec" << std::endl; - fake_prob = target_density * cross_section->TotalCrossSection(fake_record); - // std::cout << "finished sampling total xsec" << std::endl; - total_prob += fake_prob; xsec_prob += fake_prob; // Add total prob to probs @@ -266,7 +250,6 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record } } if (interactions->HasDecays()) { - // std::cout << "saw decay" << std::endl; for(auto const & decay : interactions->GetDecays() ) { for(auto const & signature : decay->GetPossibleSignaturesFromParent(record.signature.primary_type)) { fake_record.signature = signature; @@ -282,7 +265,6 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record } } } - // std::cout << "continuing...." << std::endl; if(total_prob == 0) throw(siren::utilities::InjectionFailure("No valid interactions for this event!")); // Throw a random number @@ -293,7 +275,6 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record record.signature.target_type = matching_targets[index]; record.signature = matching_signatures[index]; double selected_prob = 0.0; - // std::cout << "finished initializing stuff" << std::endl; for(unsigned int i=0; i 0 ? probs[i] - probs[i - 1] : probs[i]); @@ -303,14 +284,9 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record throw(siren::utilities::InjectionFailure("No valid interactions for this event!")); record.target_mass = detector_model->GetTargetMass(record.signature.target_type); siren::dataclasses::CrossSectionDistributionRecord xsec_record(record); - // std::cout << "finished sampling from primary process" << std::endl; if(r <= xsec_prob) { - // std::cout << "about to sample primary process final state" << std::endl; - matching_cross_sections[index]->SampleFinalState(xsec_record, random); } else { - // std::cout << "about to sample primary process final state" << std::endl; - matching_decays[index - matching_cross_sections.size()]->SampleFinalState(xsec_record, random); } xsec_record.Finalize(record); @@ -321,8 +297,6 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record // // Modifies an InteractionRecord with the new event siren::dataclasses::InteractionRecord Injector::SampleSecondaryProcess(siren::dataclasses::SecondaryDistributionRecord & secondary_record) const { - // std::cout << "sampling secondary" << std::endl; - // std::cout << "secondary record type is " << secondary_record.type << " " << secondary_record.id << std::endl; std::shared_ptr secondary_process = secondary_process_map.at(secondary_record.type); std::shared_ptr secondary_interactions = secondary_process->GetInteractions(); std::vector> secondary_distributions = secondary_process->GetSecondaryInjectionDistributions(); @@ -332,32 +306,15 @@ siren::dataclasses::InteractionRecord Injector::SampleSecondaryProcess(siren::da size_t failed_tries = 0; while(true) { tries += 1; - // // std::cout << "gotcha" << std::endl; - // for(auto & distribution : secondary_distributions) { - // // std::cout << "sample distribution" << std::endl; - // distribution->Sample(random, detector_model, secondary_process->GetInteractions(), secondary_record); - // } - // siren::dataclasses::InteractionRecord record; - // secondary_record.Finalize(record); - // // // std::cout << "sample distribution" << std::endl; - - // SampleCrossSection(record, secondary_interactions); - // return record; - try { for(auto & distribution : secondary_distributions) { - // std::cout << "sample distribution" << std::endl; distribution->Sample(random, detector_model, secondary_process->GetInteractions(), secondary_record); } siren::dataclasses::InteractionRecord record; secondary_record.Finalize(record); - // // std::cout << "sample distribution" << std::endl; - SampleCrossSection(record, secondary_interactions); return record; } catch(siren::utilities::InjectionFailure const & e) { - // std::cout << "caught error" << std::endl; - failed_tries += 1; if(failed_tries > max_tries) { throw(siren::utilities::InjectionFailure("Failed to generate secondary process!")); @@ -379,7 +336,6 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { size_t tries = 0; size_t failed_tries = 0; // Initial Process - // std::cout << "Sampling primary interactions" << std::endl; while(true) { tries += 1; try { @@ -388,7 +344,6 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { distribution->Sample(random, detector_model, primary_process->GetInteractions(), primary_record); } primary_record.Finalize(record); - // std::cout << "primary record fixed" << std:: endl; SampleCrossSection(record); break; } catch(siren::utilities::InjectionFailure const & e) { @@ -408,12 +363,9 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { std::shared_ptr parent = tree.add_entry(record); // Secondary Processes - // std::cout << "Sampling primary interactions 2" << std::endl; - std::deque, std::shared_ptr>> secondaries; std::function)> add_secondaries = [&](std::shared_ptr parent) { for(size_t i=0; irecord.signature.secondary_types.size(); ++i) { - // // std::cout << "for loop 1" << std::endl; siren::dataclasses::ParticleType const & type = parent->record.signature.secondary_types[i]; std::map>::iterator it = secondary_process_map.find(type); if(it == secondary_process_map.end()) { @@ -430,70 +382,18 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { }; add_secondaries(parent); - // std::cout << "num secondaries: " << secondaries.size() << std::endl; while(secondaries.size() > 0) { - // // std::cout << "while loop 1" << std::endl; - for(int i = secondaries.size() - 1; i >= 0; --i) { - // // std::cout << "for loop 2" << std::endl; - std::shared_ptr parent = std::get<0>(secondaries[i]); std::shared_ptr secondary_dist = std::get<1>(secondaries[i]); - // // std::cout << "for loop 2-1" << std::endl; secondaries.erase(secondaries.begin() + i); - - // std::cout << "Primary Type: " << secondary_dist->record.signature.primary_type << std::endl; - // std::cout << "Secondary Types: "; - // for (const auto& type : secondary_dist->record.signature.secondary_types) { - // std::cout << type << " "; - // } - // std::cout << std::endl; - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // this try-catch clock is to debug the secondary process LE no available interaction error - try{ - siren::dataclasses::InteractionRecord secondary_record = SampleSecondaryProcess(*secondary_dist); - std::shared_ptr secondary_datum = tree.add_entry(secondary_record, parent); - add_secondaries(secondary_datum); - } catch (const std::exception& e) { - std::cerr << "Error occurred: " << e.what() << std::endl; - - // Print the primary type and secondary types for debugging - std::cerr << "Primary Type: " << secondary_dist->record.signature.primary_type << std::endl; - std::cerr << "Secondary Types: "; - for (const auto& type : secondary_dist->record.signature.secondary_types) { - std::cerr << type << " "; - } - std::cerr << std::endl; - - // Print the primary momentum - std::cerr << "Primary Momentum: "; - for (double component : secondary_dist->record.primary_momentum) { - std::cerr << component << " "; - } - std::cerr << std::endl; - - // Print the secondary IDs - std::cerr << "Secondary IDs: "; - for (const auto& id : secondary_dist->record.secondary_ids) { - std::cerr << id << " "; - } - std::cerr << std::endl; - throw; - } catch (...) { - std::cerr << "Unknown exception caught!" << std::endl; - throw; - } - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - + siren::dataclasses::InteractionRecord secondary_record = SampleSecondaryProcess(*secondary_dist); + std::shared_ptr secondary_datum = tree.add_entry(secondary_record, parent); + add_secondaries(secondary_datum); + } - // // std::cout << "while loop 1-2" << std::endl; - } injected_events += 1; return tree; diff --git a/projects/injection/private/Weighter.cxx b/projects/injection/private/Weighter.cxx index afd11cfb8..bd166909a 100644 --- a/projects/injection/private/Weighter.cxx +++ b/projects/injection/private/Weighter.cxx @@ -114,7 +114,7 @@ double Weighter::EventWeight(siren::dataclasses::InteractionTree const & tree) c double generation_probability = injectors[idx]->EventsToInject();//GenerationProbability(tree); for(auto const & datum : tree.tree) { // skip weighting if record contains hadronization - if (isQuark(datum->record.signature.primary_type)) { + if (datum->record.signature.target_type == siren::dataclasses::Particle::ParticleType::Hadronization) { continue; } std::tuple bounds; diff --git a/projects/interactions/private/CharmDISFromSpline.cxx b/projects/interactions/private/CharmDISFromSpline.cxx index d24740568..42189f5a4 100644 --- a/projects/interactions/private/CharmDISFromSpline.cxx +++ b/projects/interactions/private/CharmDISFromSpline.cxx @@ -50,16 +50,10 @@ bool kinematicallyAllowed(double x, double y, double E, double M, double m) { //the numerator of b (or b*d) double bd = sqrt(term * term - ((m * m) / (E * E))); - // also try the D-Meson Mass? double s = 2 * M * E; double Q2 = s * x * y; double Mc = siren::utilities::Constants::D0Mass; - // if ((Q2 / (1 - x) + pow(M, 2) < pow(M + Mc, 2))) { - // std::cout << "SIREN D Meson constraint is trigged!" << std::endl; - // } return ((ad - bd) <= d * y and d * y <= (ad + bd)) && (Q2 / (1 - x) + pow(M, 2) >= pow(M + Mc, 2)); //Eq. 7 - // return ((ad - bd) <= d * y and d * y <= (ad + bd)); //Eq. 7 - } } @@ -197,8 +191,6 @@ void CharmDISFromSpline::ReadParamsFromSplineTable() { // returns true if successfully read minimum Q2 bool q2_good = differential_cross_section_.read_key("Q2MIN", minimum_Q2_); - // std::cout << "reading results: " << mass_good << " " << int_good << " " << q2_good << std::endl; - if(!int_good) { // assume DIS to preserve compatability with previous versions interaction_type_ = 1; @@ -221,9 +213,7 @@ void CharmDISFromSpline::ReadParamsFromSplineTable() { } } else { - // // std::cout << "mass and int both not good" << std::endl; if(differential_cross_section_.get_ndim() == 3) { - // std::cout << "dim is 3" << std::endl; target_mass_ = siren::utilities::Constants::isoscalarMass; } else if(differential_cross_section_.get_ndim() == 2) { @@ -233,8 +223,6 @@ void CharmDISFromSpline::ReadParamsFromSplineTable() { } } } - - // std::cout << "target mass is " << target_mass_ << std::endl; } void CharmDISFromSpline::InitializeSignatures() { @@ -293,7 +281,6 @@ double CharmDISFromSpline::TotalCrossSection(dataclasses::InteractionRecord cons rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); double primary_energy; primary_energy = interaction.primary_momentum[0]; - // std::cout << "primary energy is " << primary_energy << std::endl; // if we are below threshold, return 0 if(primary_energy < InteractionThreshold(interaction)) return 0; @@ -304,7 +291,6 @@ double CharmDISFromSpline::TotalCrossSection(siren::dataclasses::ParticleType pr if(not primary_types_.count(primary_type)) { throw std::runtime_error("Supplied primary not supported by cross section!"); } - // std::cout << "now in real sample total xsec func" << std::endl; double log_energy = log10(primary_energy); if(log_energy < total_cross_section_.lower_extent(0) @@ -316,9 +302,7 @@ double CharmDISFromSpline::TotalCrossSection(siren::dataclasses::ParticleType pr } int center; - // std::cout << "maybe problem is here?" << std::endl; total_cross_section_.searchcenters(&log_energy, ¢er); - // std::cout << "maybe problem is here??" << std::endl; double log_xs = total_cross_section_.ndsplineeval(&log_energy, ¢er, 0); @@ -355,7 +339,6 @@ double CharmDISFromSpline::DifferentialCrossSection(dataclasses::InteractionReco } double CharmDISFromSpline::DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2) const { - bool check_criteria = false; // this is used to gauge kinematic constraint in xsec and SIREN impementations, will eventually be deleted double log_energy = log10(energy); // check preconditions if(log_energy < differential_cross_section_.lower_extent(0) @@ -375,12 +358,9 @@ double CharmDISFromSpline::DifferentialCrossSection(double energy, double x, dou if(Q2 < minimum_Q2_) // cross section not calculated, assumed to be zero return 0; - if (!check_criteria) { - if(!kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) { - return 0; - } + if(!kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) { + return 0; } - std::array coordinates{{log_energy, log10(x), log10(y)}}; std::array centers; if(!differential_cross_section_.searchcenters(coordinates.data(), centers.data())) @@ -388,30 +368,6 @@ double CharmDISFromSpline::DifferentialCrossSection(double energy, double x, dou double result = pow(10., differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0)); assert(result >= 0); - if (check_criteria) { - // this is a check of kinematic constraint implementation - if (result == 0) { - if(kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) { - std::cout << "xsec gives 0 but kinematically allowed!" << std::endl; - } - } - - if(!kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) { - // check if this is due to charm production constraint - double M = target_mass_; - double E = energy; - double s = 2 * M * E; - double Q2 = s * x * y; - double Mc = siren::utilities::Constants::D0Mass; - if ((Q2 / (1 - x) + pow(M, 2) < pow(M + Mc, 2))) { // if so check result - if (result != 0) { - std::cout << "SIREN constraint not passed but xsec does not give 0!" << std::endl; - } - } - return 0; - } - } - return unit * result; @@ -428,7 +384,6 @@ void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR if (differential_cross_section_.get_ndim() != 3) { throw std::runtime_error("I expected 3 dimensions in the cross section spline, but got " + std::to_string(differential_cross_section_.get_ndim()) +". Maybe your fits file doesn't have the right 'INTERACTION' key?"); } - // std::cout << "in sample final state of charm DIS from spline" << std::endl; rk::P4 p1(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); rk::P4 p2(geom3::Vector3(0, 0, 0), record.target_mass); @@ -450,11 +405,9 @@ void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR double m = GetLeptonMass(record.signature.secondary_types[lepton_index]); double m1 = record.primary_mass; - // std::cout << "getting mass of primary: " << m1 << std::endl; double m3 = m; double E1_lab = p1_lab.e(); double E2_lab = p2_lab.e(); - // std::cout << "getting energy of primary: " << E1_lab << std::endl; // The out-going particle always gets at least enough energy for its rest mass double yMax = 1 - m / primary_energy; @@ -494,16 +447,12 @@ void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR // rejection sample a point which is kinematically allowed by calculation limits double trialQ; double trials = 0; - // std::cout << "do loop 1" << std::endl; do { - // std::cout << "do loop 2" << std::endl; if (trials >= 100) throw std::runtime_error("too much trials"); trials += 1; kin_vars[1] = random->Uniform(logXMin,0); kin_vars[2] = random->Uniform(logYMin,logYMax); trialQ = (2 * E1_lab * E2_lab) * pow(10., kin_vars[1] + kin_vars[2]); - // std::cout << kin_vars[1] << " " << kin_vars[2] << " " << trialQ << " " << minimum_Q2_ << std::endl; - // std::cout << primary_energy << " " << target_mass_ << " " << m << std::endl; } while(trialQ CharmHadronization::GetPossibleSi std::vector signatures; dataclasses::InteractionSignature signature; signature.primary_type = primary; - signature.target_type = siren::dataclasses::Particle::ParticleType::Decay; + signature.target_type = siren::dataclasses::Particle::ParticleType::Hadronization; signature.secondary_types.resize(2); signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx index 7f6520abf..5b92f0f1a 100644 --- a/projects/interactions/private/CharmMesonDecay.cxx +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -148,11 +148,9 @@ double CharmMesonDecay::TotalDecayWidthForFinalState(dataclasses::InteractionRec siren::dataclasses::Particle::ParticleType::EPlus, siren::dataclasses::Particle::ParticleType::NuE}; if (primary == siren::dataclasses::Particle::ParticleType::DPlus && secondaries == k0_eplus_nue) { - // branching_ratio = 0.089; branching_ratio = 1; tau = 1040 * (1e-15); } else if (primary == siren::dataclasses::Particle::ParticleType::D0 && secondaries == kminus_eplus_nue) { - // branching_ratio = 0.03538; branching_ratio = 1; tau = 410.1 * (1e-15); } @@ -296,34 +294,6 @@ void CharmMesonDecay::computeDiffGammaCDF(std::vector constants, double cdf_vector.push_back(1); pdf_vector.push_back(0); - // for debugging and plotting, print the pdf and cdf tables - // for (size_t i = 0; auto& element : cdf_Q2_nodes) { - // std::cout << element; - // // Print comma if it's not the last element - // if (++i != cdf_Q2_nodes.size()) { - // std::cout << ", "; - // } - // } - // std::cout << std::endl; - - // for (size_t i = 0; auto& element : cdf_vector) { - // std::cout << element; - // // Print comma if it's not the last element - // if (++i != cdf_vector.size()) { - // std::cout << ", "; - // } - // } - // std::cout << std::endl; - - // for (size_t i = 0; auto& element : pdf_vector) { - // std::cout << element; - // // Print comma if it's not the last element - // if (++i != pdf_vector.size()) { - // std::cout << ", "; - // } - // } - // std::cout << std::endl; - // set the spline table siren::utilities::TableData1D inverse_cdf_data; inverse_cdf_data.x = cdf_vector; @@ -339,17 +309,11 @@ void CharmMesonDecay::computeDiffGammaCDF(std::vector constants, double void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { // first obtain the constants needed for further computation from the signature - // std::cout<<"b1"< constants = FormFactorFromRecord(record); double mD = particleMass(record.signature.primary_type); double mK = particleMass(record.signature.secondary_types[0]); - // std::cout << "input masses: " << mD << " " << mK << std::endl; - // first sample a q^2 - //////////////////////////////////////////// - // computeDiffGammaCDF(constants, mD, mK);// - //////////////////////////////////////////// double rand_value_for_Q2 = random->Uniform(0, 1); double Q2 = inverseCdf(rand_value_for_Q2); @@ -358,8 +322,6 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco double sinTheta = std::sin(std::acos(cosTheta)); // set the x axis to be the D direction geom3::UnitVector3 x_dir = geom3::UnitVector3::xAxis(); - // std::cout<<"b2"<W+K/Pi decay, now treat the W->l+nu decay double ml = particleMass(record.signature.secondary_types[1]); double mnu = 0; @@ -400,11 +359,6 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco rk::P4 p4l_Wrest(P * geom3::Vector3(W_cosTheta, W_sinTheta, 0), ml); rk::P4 p4nu_Wrest(P * geom3::Vector3(-W_cosTheta, -W_sinTheta, 0), 0); - // std::cout << "momentums: " << p4l_Wrest << " " << p4nu_Wrest << std::endl; - // std::cout << "check mass of l and nu: " << p4l_Wrest.m() << " " << p4nu_Wrest.m() << std::endl; - //now rotate so they are defined wrt the lab frame W direction - // std::cout<<"b5"< & secondaries = record.GetSecondaryParticleRecords(); siren::dataclasses::SecondaryParticleRecord & kpi = secondaries[0]; siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[1]; @@ -434,56 +387,6 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco neutrino.SetMass(p4nu_lab.m()); neutrino.SetHelicity(record.primary_helicity); - // finally, we can populate the record, for implementation in prometheus, maybe add treatment of hadrons, but could be implemnted on p side - // record.secondary_momenta.resize(3); - // record.secondary_masses.resize(3); - // record.secondary_helicity.resize(3); // 0 is the hadron, 1 is the lepton, 2 is the neutrino - // // the K/pi - // record.secondary_momenta[0][0] = p4K_lab.e(); - // record.secondary_momenta[0][1] = p4K_lab.px(); - // record.secondary_momenta[0][2] = p4K_lab.py(); - // record.secondary_momenta[0][3] = p4K_lab.pz(); - // record.secondary_masses[0] = p4K_lab.m(); - // record.secondary_helicity[0] = 0; - // // the lepton - // record.secondary_momenta[1][0] = p4l_lab.e(); - // record.secondary_momenta[1][1] = p4l_lab.px(); - // record.secondary_momenta[1][2] = p4l_lab.py(); - // record.secondary_momenta[1][3] = p4l_lab.pz(); - // record.secondary_masses[1] = p4l_lab.m(); - // record.secondary_helicity[1] = 1; - // // the neutrino - // record.secondary_momenta[2][0] = p4nu_lab.e(); - // record.secondary_momenta[2][1] = p4nu_lab.px(); - // record.secondary_momenta[2][2] = p4nu_lab.py(); - // record.secondary_momenta[2][3] = p4nu_lab.pz(); - // record.secondary_masses[2] = p4nu_lab.m(); - // record.secondary_helicity[2] = 1; - - //for debug purposes - // double p4w_rest_Q2 = pow(p4W_Drest.e(), 2) - pow(p4W_Drest.px(), 2) - - // pow(p4W_Drest.py(), 2) - pow(p4W_Drest.pz(), 2); - // double p4w_lab_Q2 = pow(p4W_lab.e(), 2) - pow(p4W_lab.px(), 2) - - // pow(p4W_lab.py(), 2) - pow(p4W_lab.pz(), 2); - // std::cout << p4W_Drest.e() << " " << p4W_lab.e() << " " << PW << " " << p4W_Drest.p() << " " << p4W_Drest << std::endl; - // std::cout << p4K_Drest.e() << " " << p4K_lab.e() << " " << PK << " " << p4K_Drest.p() << " " << p4K_Drest << std::endl; - // std::cout << Q2 << " " << sqrt(Q2)<< std::endl; - // std::cout << "invariant mass of the W in two frames are " << p4w_lab_Q2 << " " << p4w_rest_Q2 << std::endl; - // std::cout << "check mass of W: " << p4W_lab.m() << " " << p4W_Drest.m() << std::endl; - // std::cout << "check mass of K: " << p4K_lab.m() << " " << p4K_Drest.m() << std::endl; - - - - // rk::P4 inv_mass_Wrest = p4l_Wrest + p4nu_Wrest; - // rk::P4 inv_mass_lab = p4l_lab + p4nu_lab; - // std::cout << "inv masses in two frames: " << pow(inv_mass_Wrest.m(), 2) << " " << pow(inv_mass_lab.m(), 2) << std::endl; - // std::cout << "using inv mass calculator: " << pow(invMass(p4l_Wrest, p4nu_Wrest), 2) << " " << pow(invMass(p4l_lab, p4nu_lab), 2) << std::endl; - // std::cout << "energy of l and nu and inv mass: " << El << " " << Enu << " " << pow(El+Enu, 2) << std::endl; - // std::cout << "momentums: " << p4l_Wrest << " " << p4nu_Wrest << std::endl; - // std::cout << "check mass of l and nu: " << p4l_Wrest.m() << " " << p4nu_Wrest.m() << std::endl; - // std::cout << "W energy in rest frame: " << pow(p4W_lab.m(), 2) << std::endl; - - } double CharmMesonDecay::FinalStateProbability(dataclasses::InteractionRecord const & record) const { diff --git a/projects/interactions/private/DMesonELoss.cxx b/projects/interactions/private/DMesonELoss.cxx index 867f7edf2..5779d77f4 100644 --- a/projects/interactions/private/DMesonELoss.cxx +++ b/projects/interactions/private/DMesonELoss.cxx @@ -14,9 +14,6 @@ #include // for P4, Boost #include // for Vector3 -#include // for splinetable -//#include - #include "SIREN/interactions/CrossSection.h" // for CrossSection #include "SIREN/dataclasses/InteractionRecord.h" // for Interactio... #include "SIREN/dataclasses/Particle.h" // for Particle @@ -131,18 +128,9 @@ double DMesonELoss::DifferentialCrossSection(dataclasses::InteractionRecord cons // rk::P4 p2(geom3::Vector3(interaction.target_momentum[1], interaction.target_momentum[2], interaction.target_momentum[3]), interaction.target_mass); double primary_energy; rk::P4 p1_lab; - // rk::P4 p2_lab; - // if(interaction.target_momentum[1] == 0 and interaction.target_momentum[2] == 0 and interaction.target_momentum[3] == 0) { primary_energy = interaction.primary_momentum[0]; p1_lab = p1; - // p2_lab = p2; - // } else { - // rk::Boost boost_start_to_lab = p2.restBoost(); - // p1_lab = boost_start_to_lab * p1; - // p2_lab = boost_start_to_lab * p2; - // primary_energy = p1_lab.e(); - // std::cout << "D Meson Diff Xsec: not in lab frame???" << std::endl; - // } + double final_energy = interaction.secondary_momenta[0][0]; double z = 1 - final_energy / primary_energy; @@ -170,8 +158,6 @@ double DMesonELoss::InteractionThreshold(dataclasses::InteractionRecord const & void DMesonELoss::SampleFinalState(dataclasses::CrossSectionDistributionRecord& interaction, std::shared_ptr random) const { - // std::cout << "In D Meson E Loss Sample Final State" << std::endl; - rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); // rk::P4 p2(geom3::Vector3(interaction.target_momentum[1], interaction.target_momentum[2], interaction.target_momentum[3]), interaction.target_mass); @@ -184,24 +170,8 @@ void DMesonELoss::SampleFinalState(dataclasses::CrossSectionDistributionRecord& double primary_energy; double Dmass = interaction.primary_mass; rk::P4 p1_lab; - // rk::P4 p2_lab; - // if(interaction.target_momentum[1] == 0 and interaction.target_momentum[2] == 0 and interaction.target_momentum[3] == 0) { p1_lab = p1; - // p2_lab = p2; primary_energy = p1_lab.e(); - // } else { - // // this is currently not implemented - // // Rest frame of p2 will be our "lab" frame - // rk::Boost boost_start_to_lab = p2.restBoost(); - // p1_lab = boost_start_to_lab * p1; - // p2_lab = boost_start_to_lab * p2; - // primary_energy = p1_lab.e(); - // // std::cout << "D Meson Energy Loss: not in lab frame???" << std::endl; - // } - // following line is wrong but i dont want to change it now fuck it. - // std::cout << " " << interaction.primary_momentum[0] << " " << interaction.primary_momentum[1] << " " << interaction.primary_momentum[2] << " " << interaction.primary_momentum[3]; - // std::cout << primary_energy << " " << pow(primary_energy, 2) - pow(Dmass, 2) << " " << - // sqrt(pow(interaction.primary_momentum[1], 2) +pow(interaction.primary_momentum[2], 2) +pow(interaction.primary_momentum[3], 2)) << std::endl; // sample an inelasticity from gaussian using Box-Muller Transform double sigma = 0.2; double z0 = 0.56; // for mesons only, for baryons it's 0.59, but not implemented yet @@ -218,8 +188,6 @@ void DMesonELoss::SampleFinalState(dataclasses::CrossSectionDistributionRecord& while (u1 == 0); u2 = random->Uniform(0, 1); double z = sigma * sqrt(-2.0 * log(u1)) * cos(2.0 * M_PI * u2) + z0; - // std::cout << z<< std::endl; - // now modify the energy of the charm hadron and the corresponding momentum final_energy = primary_energy * (1-z); if (pow(final_energy, 2) - pow(Dmass, 2) >= 0) { @@ -228,14 +196,9 @@ void DMesonELoss::SampleFinalState(dataclasses::CrossSectionDistributionRecord& accept = false; } } while (!accept); - // this might be an infinite loop??????? - // need to check if the cross section length is good enough, how to make some relevant plots? - // std::cout << final_energy << std::endl; double p3f = sqrt(pow(final_energy, 2) - pow(Dmass, 2)); double p3i = std::sqrt(std::pow(p1.px(), 2) + std::pow(p1.py(), 2) + std::pow(p1.pz(), 2)); double p_ratio = p3f / p3i; - - // std::cout << " " << p3f << " " << p3i << " " << p_ratio << std::endl; rk::P4 pf(p_ratio * geom3::Vector3(p1.px(), p1.py(), p1.pz()), Dmass); std::vector & secondaries = interaction.GetSecondaryParticleRecords(); @@ -246,17 +209,6 @@ void DMesonELoss::SampleFinalState(dataclasses::CrossSectionDistributionRecord& dmeson.SetMass(pf.m()); dmeson.SetHelicity(interaction.primary_helicity); - // interaction.secondary_momenta.resize(1); - // interaction.secondary_masses.resize(1); - // interaction.secondary_helicity.resize(1); - - // interaction.secondary_momenta[0][0] = pf.e(); // p3_energy - // interaction.secondary_momenta[0][1] = pf.px(); // p3_x - // interaction.secondary_momenta[0][2] = pf.py(); // p3_y - // interaction.secondary_momenta[0][3] = pf.pz(); // p3_z - // interaction.secondary_masses[0] = pf.m(); - - // interaction.secondary_helicity[0] = interaction.primary_helicity; } double DMesonELoss::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { diff --git a/resources/Examples/DMesonExample/DIS_D.py b/resources/Examples/DMesonExample/DIS_D.py index 8fb483a9e..c89d25b00 100644 --- a/resources/Examples/DMesonExample/DIS_D.py +++ b/resources/Examples/DMesonExample/DIS_D.py @@ -1,30 +1,38 @@ import os - +import numpy as np import siren from siren.SIREN_Controller import SIREN_Controller +import nuflux # Number of events to inject -events_to_inject = 5 +events_to_inject = 10000 # Expeirment to run experiment = "IceCube" +# physical flux model to use +physical_flux = "atmos" + # Define the controller controller = SIREN_Controller(events_to_inject, experiment, seed = 1) # Particle to inject primary_type = siren.dataclasses.Particle.ParticleType.NuMu -cross_section_model = "CSMSDISSplines" +xs_option = "" # current choices are the empty string and cutoff-"" +xsfiledir = "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/miaochenjin/CharmXS/xsec_splines/M_Muon-{}105MeV".format(xs_option) -xsfiledir = siren.utilities.get_cross_section_model_path(cross_section_model) +if xs_option == "": + spline_option = "M_Muon-105MeV" # this is to account for the fact that I put the output names of this particular spline incorrectly +else: + spline_option = "" # Cross Section Model target_type = siren.dataclasses.Particle.ParticleType.Nucleon DIS_xs = siren.interactions.CharmDISFromSpline( - os.path.join(xsfiledir, "dsdxdy_nu_CC_iso.fits"), - os.path.join(xsfiledir, "sigma_nu_CC_iso.fits"), + os.path.join(xsfiledir, "{}dsdxdynu-N-cc-HERAPDF15LO_EIG_central.fits".format(spline_option)), + os.path.join(xsfiledir, "{}sigmanu-N-cc-HERAPDF15LO_EIG_central.fits".format(spline_option)), [primary_type], [target_type], "m" ) @@ -41,9 +49,35 @@ primary_physical_distributions["mass"] = mass_dist # energy distribution -edist = siren.distributions.PowerLaw(2, 1e5, 1e10) +edist = siren.distributions.PowerLaw(2, 1e4, 1e10) primary_injection_distributions["energy"] = edist -primary_physical_distributions["energy"] = edist + +# make an atmospheric flux +flux = nuflux.makeFlux('honda2006') +nu_type=nuflux.NuMu +erange = np.logspace(2,6,100) +erange_atmo = np.logspace(2,6,100) +cosrange = np.linspace(0,1,100) +atmo_flux_tables = {} +particle = nuflux.NuMu +siren_key = siren.dataclasses.Particle.ParticleType(int(particle)) +atmo_flux_tables[siren_key] = np.zeros(len(erange)) +for i,e in enumerate(erange): + f = flux.getFlux(particle,e,cosrange) + atmo_flux_tables[siren_key][i] += 0.01*np.sum(f) * 1e4 * 2 * np.pi + +# this is for weighting the events to the astrophysical flux +if physical_flux == "astro": + edist_astro = siren.distributions.PowerLaw(2, 1e4, 1e10) + norm = 1e-18 * 1e4 * 4 * np.pi # GeV^-1 m^-2 s^-1 + edist_astro.SetNormalizationAtEnergy(norm,1e5) + primary_physical_distributions["energy"] = edist_astro +elif physical_flux == "atmos": + edist_atmo = siren.distributions.TabulatedFluxDistribution(erange_atmo,atmo_flux_tables[primary_type],True) + primary_physical_distributions["energy"] = edist_atmo +else: + primary_injection_distributions["energy"] = edist + # direction distribution direction_distribution = siren.distributions.IsotropicDirection() @@ -77,7 +111,7 @@ def add_secondary_to_controller(controller, secondary_type, secondary_xsecs, sec return secondary_collection -# secondary interactions +# # secondary interactions charms = siren.dataclasses.Particle.ParticleType.Charm DPlus = siren.dataclasses.Particle.ParticleType.DPlus D0 = siren.dataclasses.Particle.ParticleType.D0 @@ -90,14 +124,15 @@ def add_secondary_to_controller(controller, secondary_type, secondary_xsecs, sec secondary_DPlus_collection = add_secondary_to_controller(controller, DPlus, D_energy_loss, DPlus_decay) secondary_D0_collection = add_secondary_to_controller(controller, D0, D_energy_loss, D0_decay) +# secondary_DPlus_collection = add_secondary_to_controller(controller, DPlus, DPlus_decay) +# secondary_D0_collection = add_secondary_to_controller(controller, D0, D0_decay) + controller.SetInteractions(primary_xs, [secondary_charm_collection, secondary_D0_collection, secondary_DPlus_collection]) +# controller.SetInteractions(primary_xs, [secondary_charm_collection]) +# controller.SetInteractions(primary_xs, []) controller.Initialize() -# def stop(datum, i): -# secondary_type = datum.record.signature.secondary_types[i] -# return ((secondary_type != siren.dataclasses.Particle.ParticleType.Charm) and (secondary_type != siren.dataclasses.Particle.ParticleType.DPlus)) - def stop(datum, i): return False @@ -105,6 +140,12 @@ def stop(datum, i): events = controller.GenerateEvents() -os.makedirs("output", exist_ok=True) +print("finished generating events") + +outdir = "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/miaochenjin/DBSearch/SIREN_outputs" +expname = "0819_LE_debug_CharmHadron_atmos" +savedir = os.path.join(outdir, expname) + +os.makedirs(savedir, exist_ok=True) -controller.SaveEvents("output/FullSim") +controller.SaveEvents("{}/{}_".format(savedir, expname)) diff --git a/resources/Examples/DMesonExample/make_plots.py b/resources/Examples/DMesonExample/make_plots.py deleted file mode 100644 index 52d4dad98..000000000 --- a/resources/Examples/DMesonExample/make_plots.py +++ /dev/null @@ -1,250 +0,0 @@ -import h5py -import numpy as np -from matplotlib import pyplot as plt -from matplotlib.colors import LogNorm -from mpl_toolkits.axes_grid1 import make_axes_locatable -from parse_output import analysis -import os - -pathname = "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/miaochenjin/DBSearch/SIREN_outputs/" -# parquetname = "0708_test/0708_test_.parquet" -expname = "0709_astro_flux" -parquetname = "{}/{}_.parquet".format(expname, expname) - - -filename = os.path.join(pathname, parquetname) -savedir = os.path.join(pathname, "plots/") - -plt.style.use('paper.mplstyle') - -sim = analysis(filename) - - -c = 3 * 1e8 # m/s -m_D0 = 1.86962 # GeV -m_Dp = 1.86484 -t_Dp = 1040 * 1e-15 # s -t_D0 = 410 * 1e-15 -m_ice = 18.02 # g/mol -N = 6.02214 * 1e23 #mol^-1 -rho = 0.917 # g/cm^3 - - -def normalize(hist, xbins, ybins): - normed_hist = np.zeros_like(hist) - for i in range(len(xbins) - 1): - tot = 0 - for j in range(len(ybins) - 1): - tot += hist[i][j] - for j in range(len(ybins) - 1): - if tot != 0: - normed_hist[i][j] = hist[i][j] / tot - else: - normed_hist[i][j] = 0 - return normed_hist - -def analytic_decay_length(E, t, m): - return E * t / ((m/(c ** 2)) * c) - -def xsec(E): - return (np.exp(1.891 + 0.2095 * np.log10(E)) - 2.157 + 1.263 * np.log10(E)) * 1e-27 # convert to cm^2 - -def analytic_free_path(E): - return (m_ice / (rho * N * xsec(E))) / 100 # convert to m - -def plot_separation_distribution(analysis_, dim = 2): - D0_energies, D0_separations, Dp_energies, Dp_separations, D0_weights, Dp_weights = analysis_.separation_analysis() - min_eng = 1e1 - max_eng = 1e9 - energy_bins = np.logspace(np.log10(min_eng), np.log10(max_eng), 20) - - energy_bins_centers = np.zeros((len(energy_bins) - 1,)) - for i in range(len(energy_bins_centers)): - energy_bins_centers[i] = np.sqrt(energy_bins[i] * energy_bins[i + 1]) - D0_analytic_lengths = analytic_decay_length(energy_bins_centers, t_D0, m_D0) - Dp_analytic_lengths = analytic_decay_length(energy_bins_centers, t_Dp, m_Dp) - - min_sep = 1e-3 - max_sep = 50000 - log_separation_bins = np.logspace(np.log10(min_sep), np.log10(max_sep), 20) - sep_bin_widths = np.sqrt(log_separation_bins[1:] * log_separation_bins[:-1]) - - if dim == 2: - - X2, Y2 = np.meshgrid(energy_bins, log_separation_bins) - log_hist_D0, _, _ = np.histogram2d(D0_energies, D0_separations, bins = (energy_bins, log_separation_bins), weights = D0_weights) - log_hist_Dp, _, _ = np.histogram2d(Dp_energies, Dp_separations, bins = (energy_bins, log_separation_bins), weights = Dp_weights) - log_hist_D0 = normalize(log_hist_D0, energy_bins, log_separation_bins) - log_hist_Dp = normalize(log_hist_Dp, energy_bins, log_separation_bins) - - fig, axes = plt.subplots(nrows = 1, ncols = 2, figsize = (11, 5)) - - log_im1 = axes[0].pcolor(X2, Y2, log_hist_D0.T, cmap="plasma", alpha = 0.7, vmin=0, vmax=1) - log_im2 = axes[1].pcolor(X2, Y2, log_hist_Dp.T, cmap="plasma", alpha = 0.7, vmin=0, vmax=1) - - # divider1 = make_axes_locatable(axes[0]) - # cax1 = divider1.append_axes('right', size='5%', pad=0.05) - divider2 = make_axes_locatable(axes[1]) - cax2 = divider2.append_axes('right', size='5%', pad=0.05) - fig.colorbar(log_im2, cax=cax2, orientation='vertical', alpha = 0.7) - # fig.colorbar(log_im1, cax=cax1, orientation='vertical', alpha = 0.7) - - axes[0].set_title(r"$D^0$ Separation") - axes[1].set_title(r"$D^+$ Separation") - - axes[0].set_xlabel(r"$E_{D^0}$ [GeV]") - axes[1].set_xlabel(r"$E_{D^+}$ [GeV]") - - axes[0].set_ylabel("Separation Length [m]") - - axes[0].set_xscale('log') - axes[1].set_xscale('log') - axes[0].set_yscale('log') - axes[1].set_yscale('log') - - axes[0].set_ylim(min_sep, max_sep) - axes[1].set_ylim(min_sep, max_sep) - axes[0].set_xlim(min_eng, max_eng) - axes[1].set_xlim(min_eng, max_eng) - - # also plot the analytic lines - axes[0].plot(energy_bins_centers, D0_analytic_lengths, color = '#FEF3E8', alpha = 0.7) - axes[1].plot(energy_bins_centers, Dp_analytic_lengths, label = r"$d = \frac{E \tau}{mc}$", color = '#FEF3E8', alpha = 0.7) - - legend = axes[1].legend(loc = 'upper left') - for text in legend.get_texts(): - text.set_color('#FEF3E8') - - savename = os.path.join(savedir, "Separation_Length_Distribution") - - elif dim == 1: - - D0_separations.extend(Dp_weights) - D0_weights.extend(Dp_weights) - - sep_hist, _ = np.histogram(D0_separations, log_separation_bins, weights = D0_weights) - fig, ax = plt.subplots(1, 1, figsize = (8, 6)) - ax.hist(log_separation_bins[:-1], bins = log_separation_bins, weights = sep_hist / sep_bin_widths, \ - label = r"$\phi \sim E^{-2}$", \ - alpha = 0.9, color = '#D06C9D', histtype = 'step') - - savename = os.path.join(savedir, "PowerLaw_2_Separation_Length_Distribution") - ax.legend(loc = 'upper right') - ax.set_xscale('log') - ax.set_yscale('log') - ax.set_xlabel(r'$d_{\textrm{Sep}}$ [m]') - ax.set_ylabel('Normalized Weights Event Count') - - fig.savefig(savename, bbox_inches = 'tight') - -plot_separation_distribution(sim, dim = 1) -plot_separation_distribution(sim, dim = 2) - - -def plot_2d_energy_loss(analysis_): - E_D0, E_Dp, n_D0, n_Dp = analysis_.energy_loss_analysis_2d() - energy_bins = np.logspace(np.log10(min(min(E_D0), min(E_Dp))), np.log10(max(max(E_D0), max(E_Dp))), 20) - num_bins = np.linspace(-0.01, 7.99, 9) - X, Y = np.meshgrid(energy_bins, num_bins) - hist_D0, _, _ = np.histogram2d(E_D0, n_D0, bins = (energy_bins, num_bins)) - hist_Dp, _, _ = np.histogram2d(E_Dp, n_Dp, bins = (energy_bins, num_bins)) - - hist_D0 = normalize(hist_D0, energy_bins, num_bins) - hist_Dp = normalize(hist_Dp, energy_bins, num_bins) - - fig, axes = plt.subplots(nrows = 1, ncols = 2, figsize = (11, 5)) - im1 = axes[0].pcolor(X, Y, hist_D0.T, cmap="plasma", alpha = 0.7) - im2 = axes[1].pcolor(X, Y, hist_Dp.T, cmap="plasma", alpha = 0.7) - divider2 = make_axes_locatable(axes[1]) - cax2 = divider2.append_axes('right', size='5%', pad=0.05) - fig.colorbar(im2, cax=cax2, orientation='vertical', alpha = 0.7) - axes[0].axvline(x = 53 * 1e3, color = '#FEF3E8', alpha = 0.7, label = r"$d_{D^0} = l_{D^0}$") - axes[1].axvline(x = 22 * 1e3, color = '#FEF3E8', alpha = 0.7, label = r"$d_{D^+} = l_{D^+}$") - - axes[0].set_title(r"$D^0-p$ Collision") - axes[1].set_title(r"$D^+-p$ Collision") - - axes[0].set_xlabel(r"$E_{D^0}$ [GeV]") - axes[1].set_xlabel(r"$E_{D^+}$ [GeV]") - - axes[0].set_ylim(0, 8) - axes[1].set_ylim(0, 8) - - axes[0].set_ylabel(r"$n_{\textrm{Elastic Collision}}$") - axes[0].set_xscale('log') - axes[1].set_xscale('log') - legend0 = axes[0].legend() - legend1 = axes[1].legend() - for text in legend0.get_texts(): - text.set_color('#FEF3E8') - for text in legend1.get_texts(): - text.set_color('#FEF3E8') - - - fig.savefig("./plots/Energy_loss_2d_Distribution", bbox_inches = 'tight') - -# plot_2d_energy_loss(sim) -# exit(0) - -def plot_free_path_distribution(): - D0_E_list, D0_free_path_list, Dp_E_list, Dp_free_path_list = analysis_.free_path_analysis() - energy_bins = np.logspace(1.5, 9, 20) - distance_bins = np.logspace(-3, np.log10(5000), 20) - X, Y = np.meshgrid(energy_bins, distance_bins) - hist_D0, _, _ = np.histogram2d(D0_E_list, D0_free_path_list, bins = (energy_bins, distance_bins)) - hist_Dp, _, _ = np.histogram2d(Dp_E_list, Dp_free_path_list, bins = (energy_bins, distance_bins)) - - hist_D0 = normalize(hist_D0, energy_bins, distance_bins) - hist_Dp = normalize(hist_Dp, energy_bins, distance_bins) - - energy_bins_centers = np.zeros((len(energy_bins) - 1,)) - for i in range(len(energy_bins_centers)): - energy_bins_centers[i] = np.sqrt(energy_bins[i] * energy_bins[i + 1]) - D0_analytic_lengths = analytic_free_path(energy_bins_centers) - Dp_analytic_lengths = analytic_free_path(energy_bins_centers) - - - fig, axes = plt.subplots(nrows = 1, ncols = 2, figsize = (11, 5)) - im1 = axes[0].pcolor(X, Y, hist_D0.T, cmap="plasma", alpha = 0.7, vmin=0, vmax=1) - im2 = axes[1].pcolor(X, Y, hist_Dp.T, cmap="plasma", alpha = 0.7, vmin=0, vmax=1) - divider2 = make_axes_locatable(axes[1]) - cax2 = divider2.append_axes('right', size='5%', pad=0.05) - fig.colorbar(im2, cax=cax2, orientation='vertical', alpha = 0.7) - - axes[0].set_title(r"$D^0-p$ Free Path") - axes[1].set_title(r"$D^+-p$ Free Path") - - axes[0].set_xlabel(r"$E_{D^0}$ [GeV]") - axes[1].set_xlabel(r"$E_{D^+}$ [GeV]") - - axes[0].plot(energy_bins_centers, D0_analytic_lengths, color = '#FEF3E8', alpha = 0.7) - axes[1].plot(energy_bins_centers, Dp_analytic_lengths, label = r"$l = \frac{m_{\textrm{ice}}}{\rho N_A \sigma(E)}$", color = '#FEF3E8', alpha = 0.7) - - # also plot the decay lengths to explain low energy increase - D0_decay_analytic_lengths = analytic_decay_length(energy_bins_centers, t_D0, m_D0) - Dp_decay_analytic_lengths = analytic_decay_length(energy_bins_centers, t_Dp, m_Dp) - - axes[0].plot(energy_bins_centers, D0_decay_analytic_lengths, label = r"$d = \frac{E \tau}{mc}$", color = '#A597B6', alpha = 0.7) - axes[1].plot(energy_bins_centers, Dp_decay_analytic_lengths, color = '#A597B6', alpha = 0.7) - - axes[0].set_ylabel(r"$l_{\textrm{Free}}$") - axes[0].set_xscale('log') - axes[1].set_xscale('log') - axes[0].set_yscale('log') - axes[1].set_yscale('log') - - axes[0].set_ylim(1e-3, 5000) - axes[1].set_ylim(1e-3, 5000) - - legend0 = axes[0].legend(loc = 'upper left') - for text in legend0.get_texts(): - text.set_color('#A597B6') - - legend1 = axes[1].legend(loc = 'upper left') - for text in legend1.get_texts(): - text.set_color('#FEF3E8') - - fig.savefig("./plots/Free_Path_Distribution", bbox_inches = 'tight') - return - -# plot_free_path_distribution() \ No newline at end of file From 9f5b7d9d1bb543ea22962d0862f0c7b8cf43655c Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Thu, 22 Aug 2024 17:47:18 -0400 Subject: [PATCH 07/93] Delete resources/Examples/DMesonExample/paper.mplstyle --- .../Examples/DMesonExample/paper.mplstyle | 30 ------------------- 1 file changed, 30 deletions(-) delete mode 100644 resources/Examples/DMesonExample/paper.mplstyle diff --git a/resources/Examples/DMesonExample/paper.mplstyle b/resources/Examples/DMesonExample/paper.mplstyle deleted file mode 100644 index 77b5af0b3..000000000 --- a/resources/Examples/DMesonExample/paper.mplstyle +++ /dev/null @@ -1,30 +0,0 @@ -figure.figsize : 5, 5 # figure size in inches -savefig.dpi : 600 # figure dots per inch - -font.size: 20 -font.family: serif -font.serif: Computer Modern, Latin Modern Roman, Bitstream Vera Serif -text.usetex: True - -axes.prop_cycle: cycler('color', ['29A2C6','FF6D31','73B66B','9467BD','FFCB18', 'EF597B']) -axes.grid: False - -image.cmap : plasma - -lines.linewidth: 2 -patch.linewidth: 2 -xtick.labelsize: large -ytick.labelsize: large -xtick.minor.visible: True # visibility of minor ticks on x-axis -ytick.minor.visible: True # visibility of minor ticks on y-axis -xtick.major.size: 6 # major tick size in points -xtick.minor.size: 3 # minor tick size in points -ytick.major.size: 6 # major tick size in points -ytick.minor.size: 3 # minor tick size in points -xtick.major.width: 1 -xtick.minor.width: 1 -ytick.major.width: 1 -ytick.minor.width: 1 - -legend.frameon: False -legend.fontsize: 16 From a9c3ac1a6b173bc51b16f2f9609d9827858d1c52 Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Thu, 22 Aug 2024 17:47:25 -0400 Subject: [PATCH 08/93] Delete resources/Examples/DMesonExample/parse_output.py --- .../Examples/DMesonExample/parse_output.py | 108 ------------------ 1 file changed, 108 deletions(-) delete mode 100644 resources/Examples/DMesonExample/parse_output.py diff --git a/resources/Examples/DMesonExample/parse_output.py b/resources/Examples/DMesonExample/parse_output.py deleted file mode 100644 index c7a79fde5..000000000 --- a/resources/Examples/DMesonExample/parse_output.py +++ /dev/null @@ -1,108 +0,0 @@ -import pandas as pd -import h5py -import numpy as np -from matplotlib import pyplot as plt -from matplotlib import pyplot as plt -from matplotlib.colors import LogNorm -from mpl_toolkits.axes_grid1 import make_axes_locatable - -filename = "output/FullSim.parquet" - -def normalize(hist, xbins, ybins): - normed_hist = np.zeros_like(hist) - for i in range(len(xbins) - 1): - tot = 0 - for j in range(len(ybins) - 1): - tot += hist[i][j] - for j in range(len(ybins) - 1): - if tot != 0: - normed_hist[i][j] = hist[i][j] / tot - else: - normed_hist[i][j] = 0 - return normed_hist - -def mass(p): - return np.sqrt(p[0] ** 2 - (p[1] ** 2 + p[2] ** 2 + p[3] ** 2)) - -def decay_length(v1, v2): - return np.sqrt((v1[0] - v2[0]) ** 2 + (v1[1] - v2[1]) ** 2 + (v1[2] - v2[2]) ** 2) - -def extract_Q2(pe, pnu): - return (pe[0] + pnu[0]) ** 2 - ((pe[1] + pnu[1]) ** 2 + (pe[2] + pnu[2]) ** 2 + (pe[3] + pnu[3]) ** 2) - -def add_to_dict(list, dictionary): - for item in list: - if item in dictionary: - dictionary[item] += 1 - else: dictionary[item] = 1 - -class event: - def __init__(self, row) -> None: - self.row = row - self.num_interaction = row["num_interactions"] - - def DTYPE(self) -> int: - # type of charmed meson is the second one - return int(self.row["secondary_types"][1][1]) - - def D_DECAY_LEN(self) -> float: - # the first vertex is 0th, the decay vertex is the last one - return decay_length(self.row["vertex"][0], self.row["vertex"][-1]) - -class analysis: - def __init__(self, f) -> None: - self.df = pd.read_parquet(f) - self.num_events = len(self.df["event_weight"]) - # print("Initializing... There are {} events".format(f.attrs["num_events"])) - self.num_interactions = {} - self.secondary_types = {} - - def separation_analysis(self): - D0_energies = [] - D0_separations = [] - Dp_energies = [] - Dp_separations = [] - D0_weights = [] - Dp_weights = [] - for i in range(self.num_events): - cur_event = event(self.df.iloc[i]) - print("{}/{}".format(i, self.num_events), end = '\r') - # check if current event is D0 or D+ - if cur_event.DTYPE() == 421: # This is D0 - # extract the vertex separations - D0_separations.append(cur_event.D_DECAY_LEN()) - D0_energies.append(cur_event.row["primary_momentum"][2][0]) - D0_weights.append(cur_event.row["event_weight"]) - - elif cur_event.DTYPE() == 411: # This is D+ - # extract the vertex separations - Dp_separations.append(cur_event.D_DECAY_LEN()) - Dp_energies.append(cur_event.row["primary_momentum"][2][0]) - Dp_weights.append(cur_event.row["event_weight"]) - - return D0_energies, D0_separations, Dp_energies, Dp_separations, D0_weights, Dp_weights - - def energy_loss_analysis_2d(self): - E_D0 = [] - E_Dp = [] - n_D0 = [] - n_Dp = [] - w_D0 = [] - w_Dp = [] - for i in range(self.num_events): - cur_event = event(self.df.iloc[i]) - print("{}/{}".format(i, self.num_events), end = '\r') - if cur_event.DTYPE() == 421: # This is D0 - # extract the vertex separations - n_D0.append(cur_event.row["num_interactions"] - 3) - E_D0.append(cur_event.row["primary_momentum"][2][0]) - w_D0.append(cur_event.row["event_weight"]) - - elif cur_event.DTYPE() == 411: # This is D+ - # extract the vertex separations - n_Dp.append(cur_event.row["num_interactions"] - 3) - E_Dp.append(cur_event.row["primary_momentum"][2][0]) - w_Dp.append(cur_event.row["event_weight"]) - - return E_D0, E_Dp, n_D0, n_Dp, w_D0, w_Dp - From 8ea969d938ecfe7057f015e0f4e65d9e85c408dc Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Wed, 13 Nov 2024 13:55:47 -0500 Subject: [PATCH 09/93] update handling of numerical instabilities --- .../private/pybindings/dataclasses.cxx | 1 + .../SecondaryBoundedVertexDistribution.cxx | 5 + .../SecondaryPhysicalVertexDistribution.cxx | 5 + projects/injection/private/Injector.cxx | 4 + projects/injection/private/Weighter.cxx | 36 + projects/injection/private/WeightingUtils.cxx | 19 +- .../public/SIREN/injection/Weighter.tcc | 23 +- .../private/CharmDISFromSpline.cxx | 133 +++- .../private/CharmHadronization.cxx | 11 + projects/interactions/private/DMesonELoss.cxx | 4 + .../private/QuarkDISFromSpline.cxx | 709 ++++++++++++++++++ .../private/pybindings/CharmDISFromSpline.h | 1 + .../SIREN/interactions/CharmDISFromSpline.h | 4 + .../SIREN/interactions/QuarkDISFromSpline.h | 166 ++++ python/SIREN_Controller.py | 7 + 15 files changed, 1107 insertions(+), 21 deletions(-) create mode 100644 projects/interactions/private/QuarkDISFromSpline.cxx create mode 100644 projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h diff --git a/projects/dataclasses/private/pybindings/dataclasses.cxx b/projects/dataclasses/private/pybindings/dataclasses.cxx index a3a74f06f..2e004eabc 100644 --- a/projects/dataclasses/private/pybindings/dataclasses.cxx +++ b/projects/dataclasses/private/pybindings/dataclasses.cxx @@ -124,6 +124,7 @@ PYBIND11_MODULE(dataclasses,m) { .def(init<>()) .def("__str__", [](InteractionRecord const & r) { std::stringstream ss; ss << r; return ss.str(); }) .def_readwrite("signature",&InteractionRecord::signature) + .def_readwrite("primary_initial_position",&InteractionRecord::primary_initial_position) .def_readwrite("primary_mass",&InteractionRecord::primary_mass) .def_readwrite("primary_momentum",&InteractionRecord::primary_momentum) .def_readwrite("primary_helicity",&InteractionRecord::primary_helicity) diff --git a/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx b/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx index 43705a3ea..55bdd786e 100644 --- a/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx +++ b/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx @@ -188,6 +188,11 @@ double SecondaryBoundedVertexDistribution::GenerationProbability(std::shared_ptr prob_density = interaction_density * exp(-log_one_minus_exp_of_negative(total_interaction_depth) - traversed_interaction_depth); } + if (prob_density == 0) { + std::cout << "observed prob density 0 in physical vertex under process " << record.signature.primary_type << " to " + << record.signature.secondary_types[0] << " with total depth " << total_interaction_depth << std::endl; + } + return prob_density; } diff --git a/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx b/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx index f5d327b7a..c758fc3ba 100644 --- a/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx +++ b/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx @@ -148,6 +148,11 @@ double SecondaryPhysicalVertexDistribution::GenerationProbability(std::shared_pt prob_density = interaction_density * exp(-log_one_minus_exp_of_negative(total_interaction_depth) - traversed_interaction_depth); } + if (prob_density == 0) { + std::cout << "observed prob density 0 in physical vertex under process " << record.signature.primary_type << " to " + << record.signature.secondary_types[0] << " with total depth " << total_interaction_depth << std::endl; + } + return prob_density; } diff --git a/projects/injection/private/Injector.cxx b/projects/injection/private/Injector.cxx index a77338cff..7ca32979c 100644 --- a/projects/injection/private/Injector.cxx +++ b/projects/injection/private/Injector.cxx @@ -159,6 +159,8 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record throw(siren::utilities::InjectionFailure("No particle interaction!")); } + //std::cout << "in sample cross section" << std::endl; + std::set const & possible_targets = interactions->TargetTypes(); siren::math::Vector3D interaction_vertex( @@ -285,6 +287,7 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record record.target_mass = detector_model->GetTargetMass(record.signature.target_type); siren::dataclasses::CrossSectionDistributionRecord xsec_record(record); if(r <= xsec_prob) { + //std::cout << "going into sampel final state" << std::endl; matching_cross_sections[index]->SampleFinalState(xsec_record, random); } else { matching_decays[index - matching_cross_sections.size()]->SampleFinalState(xsec_record, random); @@ -339,6 +342,7 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { while(true) { tries += 1; try { + //std::cout << "generating primary process" << std::endl; siren::dataclasses::PrimaryDistributionRecord primary_record(primary_process->GetPrimaryType()); for(auto & distribution : primary_process->GetPrimaryInjectionDistributions()) { distribution->Sample(random, detector_model, primary_process->GetInteractions(), primary_record); diff --git a/projects/injection/private/Weighter.cxx b/projects/injection/private/Weighter.cxx index bd166909a..1aad0dc47 100644 --- a/projects/injection/private/Weighter.cxx +++ b/projects/injection/private/Weighter.cxx @@ -110,9 +110,11 @@ double Weighter::EventWeight(siren::dataclasses::InteractionTree const & tree) c double inv_weight = 0; for(unsigned int idx = 0; idx < injectors.size(); ++idx) { + // std::cout << "New Event" << std::endl; double physical_probability = 1.0; double generation_probability = injectors[idx]->EventsToInject();//GenerationProbability(tree); for(auto const & datum : tree.tree) { + // std::cout << "new depth " << datum->depth() << std::endl; // skip weighting if record contains hadronization if (datum->record.signature.target_type == siren::dataclasses::Particle::ParticleType::Hadronization) { continue; @@ -122,6 +124,17 @@ double Weighter::EventWeight(siren::dataclasses::InteractionTree const & tree) c bounds = injectors[idx]->PrimaryInjectionBounds(datum->record); physical_probability *= primary_process_weighters[idx]->PhysicalProbability(bounds, datum->record); generation_probability *= primary_process_weighters[idx]->GenerationProbability(*datum); + // for debugging purposes: nan weights are frequnetly detected + if (physical_probability == 0) { + std::cout << "zero physics depth 0: " << datum->record.signature.primary_type << std::endl; + } else if (std::isinf(physical_probability)) { + std::cout << "inf physics depth 0: " << datum->record.signature.primary_type << std::endl; + } + if (generation_probability == 0) { + std::cout << "zero gen depth 0: " << datum->record.signature.primary_type << std::endl; + } else if (std::isinf(generation_probability)) { + std::cout << "inf gen depth 0: " << datum->record.signature.primary_type << std::endl; + } } else { try { @@ -130,6 +143,16 @@ double Weighter::EventWeight(siren::dataclasses::InteractionTree const & tree) c double gen_prob = secondary_process_weighter_maps[idx].at(datum->record.signature.primary_type)->GenerationProbability(*datum); physical_probability *= phys_prob; generation_probability *= gen_prob; + // if (phys_prob == 0) { + // std::cout << "zero physics: " << datum->record.signature.primary_type << std::endl; + // } else if (std::isinf(phys_prob)) { + // std::cout << "inf physics: " << datum->record.signature.primary_type << std::endl; + // } + // if (gen_prob == 0) { + // std::cout << "zero gen: " << datum->record.signature.primary_type << std::endl; + // } else if (std::isinf(gen_prob)) { + // std::cout << "inf gen: " << datum->record.signature.primary_type << std::endl; + // } } catch(const std::out_of_range& oor) { std::cout << "Out of Range error: " << oor.what() << '\n'; return 0; @@ -137,6 +160,19 @@ double Weighter::EventWeight(siren::dataclasses::InteractionTree const & tree) c } } inv_weight += generation_probability / physical_probability; + + // if (physical_probability == 0) { + // std::cout << "Event has 0 physical probability, leading to: " << inv_weight << " " << 1./inv_weight << std::endl; + // } else if (physical_probability != physical_probability) { + // std::cout << "Event has inf physical probability, leading to: " << inv_weight << " " << 1./inv_weight << std::endl; + // } + // if (generation_probability == 0) { + // std::cout << "Event has 0 generation probability, leading to: " << inv_weight << " " << 1./inv_weight << std::endl; + // } else if (generation_probability != generation_probability) { + // std::cout << "Event has inf generation probability, leading to: " << inv_weight << " " << 1./inv_weight << std::endl; + // } + // std::cout << "gen and physics prob is " << generation_probability << " " << physical_probability << std::endl; + // std::cout << "inverse weight and final weight is " << inv_weight << " " << 1./inv_weight << std::endl; } return 1./inv_weight; } diff --git a/projects/injection/private/WeightingUtils.cxx b/projects/injection/private/WeightingUtils.cxx index 118e9139d..0b92b5cc7 100644 --- a/projects/injection/private/WeightingUtils.cxx +++ b/projects/injection/private/WeightingUtils.cxx @@ -69,15 +69,30 @@ double CrossSectionProbability(std::shared_ptr signatures = cross_section->GetPossibleSignaturesFromParents(record.signature.primary_type, target); for(auto const & signature : signatures) { + // check here for 0 generation probability fake_record.signature = signature; fake_record.target_mass = detector_model->GetTargetMass(target); // Add total cross section times density to the total prob - double target_prob = target_density * cross_section->TotalCrossSection(fake_record); + double total_xs = cross_section->TotalCrossSection(fake_record); + double target_prob = target_density * total_xs; + // if (total_xs == 0) { + // std::cout << "total cross section give 0 for process of " << record.signature.primary_type << std::endl; + // std::cout << "for signature " << fake_record.signature << std::endl; + // } else if (std::isinf(total_xs)) { + // std::cout << "total cross section give inf for process of " << record.signature.primary_type << std::endl; + // std::cout << "for signature " << fake_record.signature << std::endl; + // } total_prob += target_prob; // Add up total cross section times density times final state prob for matching signatures if(signature == record.signature) { // selected_prob += target_prob; - selected_final_state += target_prob * cross_section->FinalStateProbability(record); + double final_prob = cross_section->FinalStateProbability(record); + // if (final_prob == 0) { + // std::cout << "final state prob give 0 for process of " << record.signature.primary_type << std::endl; + // } else if (std::isinf(final_prob)) { + // std::cout << "final state prob give inf for process of " << record.signature.primary_type << std::endl; + // } + selected_final_state += target_prob * final_prob; } } } diff --git a/projects/injection/public/SIREN/injection/Weighter.tcc b/projects/injection/public/SIREN/injection/Weighter.tcc index fa85d0f55..f153be6fa 100644 --- a/projects/injection/public/SIREN/injection/Weighter.tcc +++ b/projects/injection/public/SIREN/injection/Weighter.tcc @@ -204,16 +204,28 @@ double ProcessWeighter::PhysicalProbability(std::tupleGetInteractions(), record); + // if (prob == 0) { + // std::cout << "XSec probability is 0" << std::endl; + // } physical_probability *= prob; for(auto physical_dist : unique_phys_distributions) { physical_probability *= physical_dist->GenerationProbability(detector_model, phys_process->GetInteractions(), record); + // if (physical_dist->GenerationProbability(detector_model, phys_process->GetInteractions(), record) == 0) { + // std::cout << "physical dist Generation probablity is 0" << std::endl; + // } } return normalization * physical_probability; @@ -222,10 +234,19 @@ double ProcessWeighter::PhysicalProbability(std::tuple double ProcessWeighter::GenerationProbability(siren::dataclasses::InteractionTreeDatum const & datum ) const { double gen_probability = siren::injection::CrossSectionProbability(detector_model, inj_process->GetInteractions(), datum.record); - + // if (gen_probability == 0) { + // std::cout << "Gen Cross section probability is 0" << std::endl; + // } for(auto gen_dist : unique_gen_distributions) { + // if (gen_dist->GenerationProbability(detector_model, inj_process->GetInteractions(), datum.record) == 0) { + // std::cout << "generation dist gen probability is 0" << std::endl; + // } gen_probability *= gen_dist->GenerationProbability(detector_model, inj_process->GetInteractions(), datum.record); } + + // if (gen_probability == 0) { + // std::cout << "tcc file gen prob is 0" << std::endl; + // } return gen_probability; } diff --git a/projects/interactions/private/CharmDISFromSpline.cxx b/projects/interactions/private/CharmDISFromSpline.cxx index 42189f5a4..9844231ed 100644 --- a/projects/interactions/private/CharmDISFromSpline.cxx +++ b/projects/interactions/private/CharmDISFromSpline.cxx @@ -10,6 +10,11 @@ #include // for vector #include // for assert #include // for size_t +#include +#include +#include +#include +#include #include // for P4, Boost #include // for Vector3 @@ -109,6 +114,10 @@ void CharmDISFromSpline::SetUnits(std::string units) { } } +void CharmDISFromSpline::SetInteractionType(int interaction) { + interaction_type_ = interaction; +} + bool CharmDISFromSpline::equal(CrossSection const & other) const { const CharmDISFromSpline* x = dynamic_cast(&other); @@ -186,6 +195,7 @@ double CharmDISFromSpline::GetLeptonMass(siren::dataclasses::ParticleType lepton void CharmDISFromSpline::ReadParamsFromSplineTable() { // returns true if successfully read target mass bool mass_good = differential_cross_section_.read_key("TARGETMASS", target_mass_); + if (mass_good) {std::cout << "read target mass!!" << std::endl;} // for debugging purposes // returns true if successfully read interaction type bool int_good = differential_cross_section_.read_key("INTERACTION", interaction_type_); // returns true if successfully read minimum Q2 @@ -201,6 +211,8 @@ void CharmDISFromSpline::ReadParamsFromSplineTable() { minimum_Q2_ = 1; } + std::cout << "Q2 good status is " << q2_good << "and is set to " << minimum_Q2_; + if(!mass_good) { if(int_good) { if(interaction_type_ == 1 or interaction_type_ == 2) { @@ -223,6 +235,8 @@ void CharmDISFromSpline::ReadParamsFromSplineTable() { } } } + std::cout << "target mass is " << target_mass_ << std::endl; + } void CharmDISFromSpline::InitializeSignatures() { @@ -282,8 +296,10 @@ double CharmDISFromSpline::TotalCrossSection(dataclasses::InteractionRecord cons double primary_energy; primary_energy = interaction.primary_momentum[0]; // if we are below threshold, return 0 - if(primary_energy < InteractionThreshold(interaction)) + if(primary_energy < InteractionThreshold(interaction)) { + std::cout << "DIS::interaction threshold not satisfied" << std::endl; return 0; + } return TotalCrossSection(primary_type, primary_energy); } @@ -305,6 +321,9 @@ double CharmDISFromSpline::TotalCrossSection(siren::dataclasses::ParticleType pr total_cross_section_.searchcenters(&log_energy, ¢er); double log_xs = total_cross_section_.ndsplineeval(&log_energy, ¢er, 0); + if (std::pow(10.0, log_xs) == 0) { + std::cout << "DIS::cross section evaluated to 0" << std::endl; + } return unit * std::pow(10.0, log_xs); } @@ -326,16 +345,29 @@ double CharmDISFromSpline::DifferentialCrossSection(dataclasses::InteractionReco rk::P4 q = p1 - p3; double Q2 = -q.dot(q); - double y = 1.0 - p2.dot(p3) / p2.dot(p1); - double x = Q2 / (2.0 * p2.dot(q)); - // apply slow scaling here - // double slow_scale = 1 + pow(siren::utilities::Constants::CharmMass, 2) / pow(Q2, 2); - // double xi = x * slow_scale; + double x, y; double lepton_mass = GetLeptonMass(interaction.signature.secondary_types[lepton_index]); - // return DifferentialCrossSection(primary_energy, xi, y, lepton_mass, Q2); + + y = 1.0 - p2.dot(p3) / p2.dot(p1); + x = Q2 / (2.0 * p2.dot(q)); + double log_energy = log10(primary_energy); + std::array coordinates{{log_energy, log10(x), log10(y)}}; + std::array centers; + + + if (Q2 < minimum_Q2_ || !kinematicallyAllowed(x, y, primary_energy, target_mass_, lepton_mass) + || !differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { + // std::cout << "weighting: revert back to saved x and y" << std::endl; + double E1_lab = interaction.interaction_parameters.at("energy"); + double E2_lab = p2.e(); + x = interaction.interaction_parameters.at("bjorken_x"); + y = interaction.interaction_parameters.at("bjorken_y"); + Q2 = 2. * E1_lab * E2_lab * x * y; + } return DifferentialCrossSection(primary_energy, x, y, lepton_mass, Q2); + } double CharmDISFromSpline::DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2) const { @@ -343,11 +375,16 @@ double CharmDISFromSpline::DifferentialCrossSection(double energy, double x, dou // check preconditions if(log_energy < differential_cross_section_.lower_extent(0) || log_energy>differential_cross_section_.upper_extent(0)) + {std::cout << "Diff xsec: not in bounds" << std::endl; + return 0.0;} + if(x <= 0 || x >= 1) { + std::cout << "x is out of bounds with x = " << x << std::endl; return 0.0; - if(x <= 0 || x >= 1) - return 0.0; - if(y <= 0 || y >= 1) + } + if(y <= 0 || y >= 1){ + std::cout << "y is out of bounds with x = " << y << std::endl; return 0.0; + } // we assume that: // the target is stationary so its energy is just its mass @@ -355,20 +392,27 @@ double CharmDISFromSpline::DifferentialCrossSection(double energy, double x, dou if(std::isnan(Q2)) { Q2 = 2.0 * energy * target_mass_ * x * y; } - if(Q2 < minimum_Q2_) // cross section not calculated, assumed to be zero + if(Q2 < minimum_Q2_) { + std::cout << "Q2 is smaller than minimum Q2 with " << Q2 << " < " << minimum_Q2_ << std::endl; return 0; + } // cross section not calculated, assumed to be zero if(!kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) { + std::cout << "not kinematically allowed!" << std::endl; return 0; } std::array coordinates{{log_energy, log10(x), log10(y)}}; std::array centers; - if(!differential_cross_section_.searchcenters(coordinates.data(), centers.data())) + if(!differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { + std::cout << "search centers failed!" << std::endl; return 0; + } double result = pow(10., differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0)); assert(result >= 0); - - + if (std::isinf(result)) { + std::cout << "energy, x, y, Q2 are " << energy << " " << x << " " << y << " " << Q2 << " " << std::endl; + std::cout << "spline value read is " << differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0) << std::endl; + } return unit * result; } @@ -519,12 +563,14 @@ void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR if(accept) { kin_vars = test_kin_vars; cross_section = test_cross_section; + // std::cout << "trial Q is" << trialQ << std::endl; } } - + //////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////// double final_x = pow(10., kin_vars[1]); double final_y = pow(10., kin_vars[2]); - record.interaction_parameters.clear(); record.interaction_parameters["energy"] = E1_lab; record.interaction_parameters["bjorken_x"] = final_x; @@ -534,8 +580,54 @@ void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR double p1x_lab = std::sqrt(p1_lab.px() * p1_lab.px() + p1_lab.py() * p1_lab.py() + p1_lab.pz() * p1_lab.pz()); double pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); double momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); - double pqy_lab = std::sqrt(momq_lab*momq_lab - pqx_lab *pqx_lab); - double Eq_lab = E1_lab * final_y; + double pqy_lab, Eq_lab; + + if (pqx_lab>momq_lab){ + // if current setting does not work, start looping through scalings + int maxIterations = 10; + int iteration = 0; + double p1_lab_x = p1_lab.px(); + double p1_lab_y = p1_lab.py(); + double p1_lab_z = p1_lab.pz(); + // loop to resolve precision issue + while (iteration <= maxIterations) { + Q2 = 2. * E1_lab * E2_lab * pow(10.0, kin_vars[1] + kin_vars[2]); + p1x_lab = std::sqrt(p1_lab_x * p1_lab_x + p1_lab_y * p1_lab_y + p1_lab_z * p1_lab_z); + pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); + momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); + if (pqx_lab>momq_lab){ + // std::cout << "triggered on " << momq_lab << " and " << pqx_lab << std::endl; + //scale down + E1_lab /= 10; + E2_lab /= 10; + p1_lab_x /= 10; + p1_lab_y /= 10; + p1_lab_z /= 10; + m1 /= 10; + m3 /= 10; + //iteration += 1 to scale back + iteration += 1; + continue; + } + pqy_lab = std::sqrt((momq_lab + pqx_lab) * (momq_lab - pqx_lab)); + // std::cout << "finished with " << iteration << " iterations and " << momq_lab << " and " << pqx_lab << std::endl; + break; + } + // //scale back + if (iteration > 0) { + // std::cout << "scaling back with " << pow(10.0, iteration); + E1_lab *= pow(10.0, iteration); + E2_lab *= pow(10.0, iteration); + p1_lab_x *= pow(10.0, iteration); + p1_lab_y *= pow(10.0, iteration); + p1_lab_z *= pow(10.0, iteration); + m1 *= pow(10.0, iteration); + m3 *= pow(10.0, iteration); + // std::cout << "and finished with " << momq_lab << " and " << pqx_lab << std::endl; + } + // pqy_lab = 0; + } else {pqy_lab = std::sqrt(momq_lab*momq_lab - pqx_lab *pqx_lab);} + Eq_lab = E1_lab * final_y; geom3::UnitVector3 x_dir = geom3::UnitVector3::xAxis(); geom3::Vector3 p1_mom = p1_lab.momentum(); @@ -571,10 +663,15 @@ void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR double CharmDISFromSpline::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { double dxs = DifferentialCrossSection(interaction); + // if (dxs == 0) { + // std::cout << "diff xsec gives 0" << std::endl; + // } double txs = TotalCrossSection(interaction); if(dxs == 0) { return 0.0; } else { + // if (txs == 0) {std::cout << "wtf??? txs is 0 in final state prob" << txs << std::endl;} + // if (std::isinf(dxs)) {std::cout << "dxs is inf in final state prob" << std::endl;} return dxs / txs; } } diff --git a/projects/interactions/private/CharmHadronization.cxx b/projects/interactions/private/CharmHadronization.cxx index 3da05db72..008b608ff 100644 --- a/projects/interactions/private/CharmHadronization.cxx +++ b/projects/interactions/private/CharmHadronization.cxx @@ -173,8 +173,19 @@ void CharmHadronization::SampleFinalState(dataclasses::CrossSectionDistributionR double z; double ECH; + // add a maximum number of trials in the while loop + int max_sampling = 100; + int sampling = 0; + // sample again if this eenrgy is not kinematically allowed do { + sampling += 1; + if (sampling > max_sampling) { + std::cout << "energy of the charm is " << Ec << " and momentum is " << p3c << std::endl; + std::cout << "desired mass of hadron is " << mCH << std::endl; + throw(siren::utilities::InjectionFailure("Failed to sample hadronization!")); + break; + } randValue = random->Uniform(0,1); z = inverseCdfTable(randValue); ECH = z * Ec; diff --git a/projects/interactions/private/DMesonELoss.cxx b/projects/interactions/private/DMesonELoss.cxx index 5779d77f4..6454c3a63 100644 --- a/projects/interactions/private/DMesonELoss.cxx +++ b/projects/interactions/private/DMesonELoss.cxx @@ -115,6 +115,10 @@ double DMesonELoss::TotalCrossSection(siren::dataclasses::Particle::ParticleType // current implementation uses only > 1PeV data double xsec = exp(1.891 + 0.205 * log_energy) - 2.157 + 1.264 * log_energy; + if (xsec == 0) { + std::cout << "DMesonELoss total xsec gives 0 prob!" << std::endl; + } + return xsec * mb_to_cm2; } diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx new file mode 100644 index 000000000..9844231ed --- /dev/null +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -0,0 +1,709 @@ +#include "SIREN/interactions/CharmDISFromSpline.h" + +#include // for map, opera... +#include // for set, opera... +#include // for array +#include // for pow, log10 +#include // for tie, opera... +#include // for allocator +#include // for basic_string +#include // for vector +#include // for assert +#include // for size_t +#include +#include +#include +#include +#include + +#include // for P4, Boost +#include // for Vector3 + +#include // for splinetable +//#include + +#include "SIREN/interactions/CrossSection.h" // for CrossSection +#include "SIREN/dataclasses/InteractionRecord.h" // for Interactio... +#include "SIREN/dataclasses/Particle.h" // for Particle +#include "SIREN/utilities/Random.h" // for SIREN_random +#include "SIREN/utilities/Constants.h" // for electronMass + +namespace siren { +namespace interactions { + +namespace { +///Check whether a given point in phase space is physically realizable. +///Based on equations 6-8 of http://dx.doi.org/10.1103/PhysRevD.66.113007 +///S. Kretzer and M. H. Reno +///"Tau neutrino deep inelastic charged current interactions" +///Phys. Rev. D 66, 113007 +///\param x Bjorken x of the interaction +///\param y Bjorken y of the interaction +///\param E Incoming neutrino in energy in the lab frame ($E_\nu$) +///\param M Mass of the target nucleon ($M_N$) +///\param m Mass of the secondary lepton ($m_\tau$) +bool kinematicallyAllowed(double x, double y, double E, double M, double m) { + if(x > 1) //Eq. 6 right inequality + return false; + if(x < ((m * m) / (2 * M * (E - m)))) //Eq. 6 left inequality + return false; + //denominator of a and b + double d = 2 * (1 + (M * x) / (2 * E)); + //the numerator of a (or a*d) + double ad = 1 - m * m * ((1 / (2 * M * E * x)) + (1 / (2 * E * E))); + double term = 1 - ((m * m) / (2 * M * E * x)); + //the numerator of b (or b*d) + double bd = sqrt(term * term - ((m * m) / (E * E))); + + double s = 2 * M * E; + double Q2 = s * x * y; + double Mc = siren::utilities::Constants::D0Mass; + return ((ad - bd) <= d * y and d * y <= (ad + bd)) && (Q2 / (1 - x) + pow(M, 2) >= pow(M + Mc, 2)); //Eq. 7 +} +} + +CharmDISFromSpline::CharmDISFromSpline() {} + +CharmDISFromSpline::CharmDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { + LoadFromMemory(differential_data, total_data); + InitializeSignatures(); + SetUnits(units); +} + +CharmDISFromSpline::CharmDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { + LoadFromMemory(differential_data, total_data); + InitializeSignatures(); + SetUnits(units); +} + +CharmDISFromSpline::CharmDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { + LoadFromFile(differential_filename, total_filename); + InitializeSignatures(); + SetUnits(units); +} + +CharmDISFromSpline::CharmDISFromSpline(std::string differential_filename, std::string total_filename, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types) { + LoadFromFile(differential_filename, total_filename); + ReadParamsFromSplineTable(); + InitializeSignatures(); + SetUnits(units); +} + +CharmDISFromSpline::CharmDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { + LoadFromFile(differential_filename, total_filename); + InitializeSignatures(); + SetUnits(units); +} + +CharmDISFromSpline::CharmDISFromSpline(std::string differential_filename, std::string total_filename, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()) { + LoadFromFile(differential_filename, total_filename); + ReadParamsFromSplineTable(); + InitializeSignatures(); + SetUnits(units); +} + +void CharmDISFromSpline::SetUnits(std::string units) { + std::transform(units.begin(), units.end(), units.begin(), + [](unsigned char c){ return std::tolower(c); }); + if(units == "cm") { + unit = 1.0; + } else if(units == "m") { + unit = 10000.0; + } else { + throw std::runtime_error("Cross section units not supported!"); + } +} + +void CharmDISFromSpline::SetInteractionType(int interaction) { + interaction_type_ = interaction; +} + +bool CharmDISFromSpline::equal(CrossSection const & other) const { + const CharmDISFromSpline* x = dynamic_cast(&other); + + if(!x) + return false; + else + return + std::tie( + interaction_type_, + target_mass_, + minimum_Q2_, + signatures_, + primary_types_, + target_types_, + differential_cross_section_, + total_cross_section_) + == + std::tie( + x->interaction_type_, + x->target_mass_, + x->minimum_Q2_, + x->signatures_, + x->primary_types_, + x->target_types_, + x->differential_cross_section_, + x->total_cross_section_); +} + +void CharmDISFromSpline::LoadFromFile(std::string dd_crossSectionFile, std::string total_crossSectionFile) { + + differential_cross_section_ = photospline::splinetable<>(dd_crossSectionFile.c_str()); + + if(differential_cross_section_.get_ndim()!=3 && differential_cross_section_.get_ndim()!=2) + throw std::runtime_error("cross section spline has " + std::to_string(differential_cross_section_.get_ndim()) + + " dimensions, should have either 3 (log10(E), log10(x), log10(y)) or 2 (log10(E), log10(y))"); + + total_cross_section_ = photospline::splinetable<>(total_crossSectionFile.c_str()); + + if(total_cross_section_.get_ndim() != 1) + throw std::runtime_error("Total cross section spline has " + std::to_string(total_cross_section_.get_ndim()) + + " dimensions, should have 1, log10(E)"); +} + +void CharmDISFromSpline::LoadFromMemory(std::vector & differential_data, std::vector & total_data) { + differential_cross_section_.read_fits_mem(differential_data.data(), differential_data.size()); + total_cross_section_.read_fits_mem(total_data.data(), total_data.size()); +} + +double CharmDISFromSpline::GetLeptonMass(siren::dataclasses::ParticleType lepton_type) { + int32_t lepton_number = std::abs(static_cast(lepton_type)); + double lepton_mass; + switch(lepton_number) { + case 11: + lepton_mass = siren::utilities::Constants::electronMass; + break; + case 13: + lepton_mass = siren::utilities::Constants::muonMass; + break; + case 15: + lepton_mass = siren::utilities::Constants::tauMass; + break; + case 12: + lepton_mass = 0; + case 14: + lepton_mass = 0; + case 16: + lepton_mass = 0; + break; + default: + throw std::runtime_error("Unknown lepton type!"); + } + return lepton_mass; +} + +void CharmDISFromSpline::ReadParamsFromSplineTable() { + // returns true if successfully read target mass + bool mass_good = differential_cross_section_.read_key("TARGETMASS", target_mass_); + if (mass_good) {std::cout << "read target mass!!" << std::endl;} // for debugging purposes + // returns true if successfully read interaction type + bool int_good = differential_cross_section_.read_key("INTERACTION", interaction_type_); + // returns true if successfully read minimum Q2 + bool q2_good = differential_cross_section_.read_key("Q2MIN", minimum_Q2_); + + if(!int_good) { + // assume DIS to preserve compatability with previous versions + interaction_type_ = 1; + } + + if(!q2_good) { + // assume 1 GeV^2 + minimum_Q2_ = 1; + } + + std::cout << "Q2 good status is " << q2_good << "and is set to " << minimum_Q2_; + + if(!mass_good) { + if(int_good) { + if(interaction_type_ == 1 or interaction_type_ == 2) { + target_mass_ = (siren::dataclasses::isLepton(siren::dataclasses::ParticleType::PPlus)+ + siren::dataclasses::isLepton(siren::dataclasses::ParticleType::Neutron))/2; + } else if(interaction_type_ == 3) { + target_mass_ = siren::dataclasses::isLepton(siren::dataclasses::ParticleType::EMinus); + } else { + throw std::runtime_error("Logic error. Interaction type is not 1, 2, or 3!"); + } + + } else { + if(differential_cross_section_.get_ndim() == 3) { + target_mass_ = siren::utilities::Constants::isoscalarMass; + + } else if(differential_cross_section_.get_ndim() == 2) { + target_mass_ = siren::utilities::Constants::electronMass; + } else { + throw std::runtime_error("Logic error. Spline dimensionality is not 2, or 3!"); + } + } + } + std::cout << "target mass is " << target_mass_ << std::endl; + +} + +void CharmDISFromSpline::InitializeSignatures() { + signatures_.clear(); + for(auto primary_type : primary_types_) { + dataclasses::InteractionSignature signature; + signature.primary_type = primary_type; + + if(not isNeutrino(primary_type)) { + throw std::runtime_error("This DIS implementation only supports neutrinos as primaries!"); + } + + siren::dataclasses::ParticleType charged_lepton_product = siren::dataclasses::ParticleType::unknown; + siren::dataclasses::ParticleType neutral_lepton_product = primary_type; + + if(primary_type == siren::dataclasses::ParticleType::NuE) { + charged_lepton_product = siren::dataclasses::ParticleType::EMinus; + } else if(primary_type == siren::dataclasses::ParticleType::NuEBar) { + charged_lepton_product = siren::dataclasses::ParticleType::EPlus; + } else if(primary_type == siren::dataclasses::ParticleType::NuMu) { + charged_lepton_product = siren::dataclasses::ParticleType::MuMinus; + } else if(primary_type == siren::dataclasses::ParticleType::NuMuBar) { + charged_lepton_product = siren::dataclasses::ParticleType::MuPlus; + } else if(primary_type == siren::dataclasses::ParticleType::NuTau) { + charged_lepton_product = siren::dataclasses::ParticleType::TauMinus; + } else if(primary_type == siren::dataclasses::ParticleType::NuTauBar) { + charged_lepton_product = siren::dataclasses::ParticleType::TauPlus; + } else { + throw std::runtime_error("InitializeSignatures: Unkown parent neutrino type!"); + } + + if(interaction_type_ == 1) { + signature.secondary_types.push_back(charged_lepton_product); + } else if(interaction_type_ == 2) { + signature.secondary_types.push_back(neutral_lepton_product); + } else if(interaction_type_ == 3) { + signature.secondary_types.push_back(siren::dataclasses::ParticleType::Hadrons); + } else { + throw std::runtime_error("InitializeSignatures: Unkown interaction type!"); + } + + signature.secondary_types.push_back(siren::dataclasses::ParticleType::Charm); + for(auto target_type : target_types_) { + signature.target_type = target_type; + + signatures_.push_back(signature); + + std::pair key(primary_type, target_type); + signatures_by_parent_types_[key].push_back(signature); + } + } +} + +double CharmDISFromSpline::TotalCrossSection(dataclasses::InteractionRecord const & interaction) const { + siren::dataclasses::ParticleType primary_type = interaction.signature.primary_type; + rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); + double primary_energy; + primary_energy = interaction.primary_momentum[0]; + // if we are below threshold, return 0 + if(primary_energy < InteractionThreshold(interaction)) { + std::cout << "DIS::interaction threshold not satisfied" << std::endl; + return 0; + } + return TotalCrossSection(primary_type, primary_energy); +} + +double CharmDISFromSpline::TotalCrossSection(siren::dataclasses::ParticleType primary_type, double primary_energy) const { + if(not primary_types_.count(primary_type)) { + throw std::runtime_error("Supplied primary not supported by cross section!"); + } + double log_energy = log10(primary_energy); + + if(log_energy < total_cross_section_.lower_extent(0) + or log_energy > total_cross_section_.upper_extent(0)) { + throw std::runtime_error("Interaction energy ("+ std::to_string(primary_energy) + + ") out of cross section table range: [" + + std::to_string(pow(10.,total_cross_section_.lower_extent(0))) + " GeV," + + std::to_string(pow(10.,total_cross_section_.upper_extent(0))) + " GeV]"); + } + + int center; + total_cross_section_.searchcenters(&log_energy, ¢er); + + double log_xs = total_cross_section_.ndsplineeval(&log_energy, ¢er, 0); + if (std::pow(10.0, log_xs) == 0) { + std::cout << "DIS::cross section evaluated to 0" << std::endl; + } + + return unit * std::pow(10.0, log_xs); +} + +double CharmDISFromSpline::DifferentialCrossSection(dataclasses::InteractionRecord const & interaction) const { + rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); + rk::P4 p2(geom3::Vector3(0, 0, 0), interaction.target_mass); + double primary_energy; + primary_energy = interaction.primary_momentum[0]; + assert(interaction.signature.secondary_types.size() == 2); + unsigned int lepton_index = (isLepton(interaction.signature.secondary_types[0])) ? 0 : 1; + unsigned int other_index = 1 - lepton_index; + + std::array const & mom3 = interaction.secondary_momenta[lepton_index]; + std::array const & mom4 = interaction.secondary_momenta[other_index]; + rk::P4 p3(geom3::Vector3(mom3[1], mom3[2], mom3[3]), interaction.secondary_masses[lepton_index]); + rk::P4 p4(geom3::Vector3(mom4[1], mom4[2], mom4[3]), interaction.secondary_masses[other_index]); + + rk::P4 q = p1 - p3; + + double Q2 = -q.dot(q); + double x, y; + double lepton_mass = GetLeptonMass(interaction.signature.secondary_types[lepton_index]); + + + y = 1.0 - p2.dot(p3) / p2.dot(p1); + x = Q2 / (2.0 * p2.dot(q)); + double log_energy = log10(primary_energy); + std::array coordinates{{log_energy, log10(x), log10(y)}}; + std::array centers; + + + if (Q2 < minimum_Q2_ || !kinematicallyAllowed(x, y, primary_energy, target_mass_, lepton_mass) + || !differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { + // std::cout << "weighting: revert back to saved x and y" << std::endl; + double E1_lab = interaction.interaction_parameters.at("energy"); + double E2_lab = p2.e(); + x = interaction.interaction_parameters.at("bjorken_x"); + y = interaction.interaction_parameters.at("bjorken_y"); + Q2 = 2. * E1_lab * E2_lab * x * y; + } + return DifferentialCrossSection(primary_energy, x, y, lepton_mass, Q2); + + +} + +double CharmDISFromSpline::DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2) const { + double log_energy = log10(energy); + // check preconditions + if(log_energy < differential_cross_section_.lower_extent(0) + || log_energy>differential_cross_section_.upper_extent(0)) + {std::cout << "Diff xsec: not in bounds" << std::endl; + return 0.0;} + if(x <= 0 || x >= 1) { + std::cout << "x is out of bounds with x = " << x << std::endl; + return 0.0; + } + if(y <= 0 || y >= 1){ + std::cout << "y is out of bounds with x = " << y << std::endl; + return 0.0; + } + + // we assume that: + // the target is stationary so its energy is just its mass + // the incoming neutrino is massless, so its kinetic energy is its total energy + if(std::isnan(Q2)) { + Q2 = 2.0 * energy * target_mass_ * x * y; + } + if(Q2 < minimum_Q2_) { + std::cout << "Q2 is smaller than minimum Q2 with " << Q2 << " < " << minimum_Q2_ << std::endl; + return 0; + } // cross section not calculated, assumed to be zero + + if(!kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) { + std::cout << "not kinematically allowed!" << std::endl; + return 0; + } + std::array coordinates{{log_energy, log10(x), log10(y)}}; + std::array centers; + if(!differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { + std::cout << "search centers failed!" << std::endl; + return 0; + } + double result = pow(10., differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0)); + assert(result >= 0); + if (std::isinf(result)) { + std::cout << "energy, x, y, Q2 are " << energy << " " << x << " " << y << " " << Q2 << " " << std::endl; + std::cout << "spline value read is " << differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0) << std::endl; + } + + return unit * result; +} + +double CharmDISFromSpline::InteractionThreshold(dataclasses::InteractionRecord const & interaction) const { + // Consider implementing DIS thershold at some point + return 0; +} + +void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { + // Uses Metropolis-Hastings Algorithm! + // useful for cases where we don't know the supremum of our distribution, and the distribution is multi-dimensional + if (differential_cross_section_.get_ndim() != 3) { + throw std::runtime_error("I expected 3 dimensions in the cross section spline, but got " + std::to_string(differential_cross_section_.get_ndim()) +". Maybe your fits file doesn't have the right 'INTERACTION' key?"); + } + rk::P4 p1(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); + rk::P4 p2(geom3::Vector3(0, 0, 0), record.target_mass); + + // we assume that: + // the target is stationary so its energy is just its mass + // the incoming neutrino is massless, so its kinetic energy is its total energy + // double s = target_mass_ * trecord.secondary_momentarget_mass_ + 2 * target_mass_ * primary_energy; + // double s = std::pow(rk::invMass(p1, p2), 2); + + double primary_energy; + rk::P4 p1_lab; + rk::P4 p2_lab; + p1_lab = p1; + p2_lab = p2; + primary_energy = p1_lab.e(); + + unsigned int lepton_index = (isLepton(record.signature.secondary_types[0])) ? 0 : 1; + unsigned int other_index = 1 - lepton_index; + double m = GetLeptonMass(record.signature.secondary_types[lepton_index]); + + double m1 = record.primary_mass; + double m3 = m; + double E1_lab = p1_lab.e(); + double E2_lab = p2_lab.e(); + + // The out-going particle always gets at least enough energy for its rest mass + double yMax = 1 - m / primary_energy; + double logYMax = log10(yMax); + + // The minimum allowed value of y occurs when x = 1 and Q is minimized + double yMin = minimum_Q2_ / (2 * E1_lab * E2_lab); + double logYMin = log10(yMin); + // The minimum allowed value of x occurs when y = yMax and Q is minimized + // double xMin = minimum_Q2_ / ((s - target_mass_ * target_mass_) * yMax); + double xMin = minimum_Q2_ / (2 * E1_lab * E2_lab * yMax); + double logXMin = log10(xMin); + + bool accept; + + // kin_vars and its twin are 3-vectors containing [nu-energy, Bjorken X, Bjorken Y] + std::array kin_vars, test_kin_vars; + + // centers of the cross section spline tales. + std::array spline_table_center, test_spline_table_center; + + // values of cross_section from the splines. By * Bx * Spline(E,x,y) + double cross_section, test_cross_section; + + // No matter what, we're evaluating at this specific energy. + kin_vars[0] = test_kin_vars[0] = log10(primary_energy); + + // check preconditions + if(kin_vars[0] < differential_cross_section_.lower_extent(0) + || kin_vars[0] > differential_cross_section_.upper_extent(0)) + throw std::runtime_error("Interaction energy out of cross section table range: [" + + std::to_string(pow(10.,differential_cross_section_.lower_extent(0))) + " GeV," + + std::to_string(pow(10.,differential_cross_section_.upper_extent(0))) + " GeV]"); + + // sample an intial point + do { + // rejection sample a point which is kinematically allowed by calculation limits + double trialQ; + double trials = 0; + do { + if (trials >= 100) throw std::runtime_error("too much trials"); + trials += 1; + kin_vars[1] = random->Uniform(logXMin,0); + kin_vars[2] = random->Uniform(logYMin,logYMax); + trialQ = (2 * E1_lab * E2_lab) * pow(10., kin_vars[1] + kin_vars[2]); + } while(trialQ differential_cross_section_.upper_extent(1)) { + accept = false; + } + if(kin_vars[2] < differential_cross_section_.lower_extent(2) + || kin_vars[2] > differential_cross_section_.upper_extent(2)) { + accept = false; + } + + if(accept) { + // finds the centers in the cross section spline table, returns true if it's successful + // also sets the centers + accept = differential_cross_section_.searchcenters(kin_vars.data(),spline_table_center.data()); + } + } while(!accept); + + //TODO: better proposal distribution? + double measure = pow(10., kin_vars[1] + kin_vars[2]); // Bx * By + + // Bx * By * xs(E, x, y) + // evalutates the differential spline at that point + cross_section = measure*pow(10., differential_cross_section_.ndsplineeval(kin_vars.data(), spline_table_center.data(), 0)); + + // this is the magic part. Metropolis Hastings Algorithm. + // MCMC method! + const size_t burnin = 40; // converges to the correct distribution over multiple samplings. + // big number means more accurate, but slower + for(size_t j = 0; j <= burnin; j++) { + // repeat the sampling from above to get a new valid point + double trialQ; + do { + test_kin_vars[1] = random->Uniform(logXMin, 0); + test_kin_vars[2] = random->Uniform(logYMin, logYMax); + trialQ = (2 * E1_lab * E2_lab) * pow(10., test_kin_vars[1] + test_kin_vars[2]); + } while(trialQ < minimum_Q2_ || !kinematicallyAllowed(pow(10., test_kin_vars[1]), pow(10., test_kin_vars[2]), primary_energy, target_mass_, m)); + + accept = true; + if(test_kin_vars[1] < differential_cross_section_.lower_extent(1) + || test_kin_vars[1] > differential_cross_section_.upper_extent(1)) + accept = false; + if(test_kin_vars[2] < differential_cross_section_.lower_extent(2) + || test_kin_vars[2] > differential_cross_section_.upper_extent(2)) + accept = false; + if(!accept) + continue; + + accept = differential_cross_section_.searchcenters(test_kin_vars.data(), test_spline_table_center.data()); + if(!accept) + continue; + + double measure = pow(10., test_kin_vars[1] + test_kin_vars[2]); + double eval = differential_cross_section_.ndsplineeval(test_kin_vars.data(), test_spline_table_center.data(), 0); + if(std::isnan(eval)) + continue; + test_cross_section = measure * pow(10., eval); + + double odds = (test_cross_section / cross_section); + accept = (cross_section == 0 || (odds > 1.) || random->Uniform(0, 1) < odds); + + if(accept) { + kin_vars = test_kin_vars; + cross_section = test_cross_section; + // std::cout << "trial Q is" << trialQ << std::endl; + } + } + //////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////// + double final_x = pow(10., kin_vars[1]); + double final_y = pow(10., kin_vars[2]); + record.interaction_parameters.clear(); + record.interaction_parameters["energy"] = E1_lab; + record.interaction_parameters["bjorken_x"] = final_x; + record.interaction_parameters["bjorken_y"] = final_y; + + double Q2 = 2 * E1_lab * E2_lab * pow(10.0, kin_vars[1] + kin_vars[2]); + double p1x_lab = std::sqrt(p1_lab.px() * p1_lab.px() + p1_lab.py() * p1_lab.py() + p1_lab.pz() * p1_lab.pz()); + double pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); + double momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); + double pqy_lab, Eq_lab; + + if (pqx_lab>momq_lab){ + // if current setting does not work, start looping through scalings + int maxIterations = 10; + int iteration = 0; + double p1_lab_x = p1_lab.px(); + double p1_lab_y = p1_lab.py(); + double p1_lab_z = p1_lab.pz(); + // loop to resolve precision issue + while (iteration <= maxIterations) { + Q2 = 2. * E1_lab * E2_lab * pow(10.0, kin_vars[1] + kin_vars[2]); + p1x_lab = std::sqrt(p1_lab_x * p1_lab_x + p1_lab_y * p1_lab_y + p1_lab_z * p1_lab_z); + pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); + momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); + if (pqx_lab>momq_lab){ + // std::cout << "triggered on " << momq_lab << " and " << pqx_lab << std::endl; + //scale down + E1_lab /= 10; + E2_lab /= 10; + p1_lab_x /= 10; + p1_lab_y /= 10; + p1_lab_z /= 10; + m1 /= 10; + m3 /= 10; + //iteration += 1 to scale back + iteration += 1; + continue; + } + pqy_lab = std::sqrt((momq_lab + pqx_lab) * (momq_lab - pqx_lab)); + // std::cout << "finished with " << iteration << " iterations and " << momq_lab << " and " << pqx_lab << std::endl; + break; + } + // //scale back + if (iteration > 0) { + // std::cout << "scaling back with " << pow(10.0, iteration); + E1_lab *= pow(10.0, iteration); + E2_lab *= pow(10.0, iteration); + p1_lab_x *= pow(10.0, iteration); + p1_lab_y *= pow(10.0, iteration); + p1_lab_z *= pow(10.0, iteration); + m1 *= pow(10.0, iteration); + m3 *= pow(10.0, iteration); + // std::cout << "and finished with " << momq_lab << " and " << pqx_lab << std::endl; + } + // pqy_lab = 0; + } else {pqy_lab = std::sqrt(momq_lab*momq_lab - pqx_lab *pqx_lab);} + Eq_lab = E1_lab * final_y; + + geom3::UnitVector3 x_dir = geom3::UnitVector3::xAxis(); + geom3::Vector3 p1_mom = p1_lab.momentum(); + geom3::UnitVector3 p1_lab_dir = p1_mom.direction(); + geom3::Rotation3 x_to_p1_lab_rot = geom3::rotationBetween(x_dir, p1_lab_dir); + + double phi = random->Uniform(0, 2.0 * M_PI); + geom3::Rotation3 rand_rot(p1_lab_dir, phi); + + rk::P4 pq_lab(Eq_lab, geom3::Vector3(pqx_lab, pqy_lab, 0)); + pq_lab.rotate(x_to_p1_lab_rot); + pq_lab.rotate(rand_rot); + + rk::P4 p3_lab((p1_lab - pq_lab).momentum(), m3); + rk::P4 p4_lab = p2_lab + pq_lab; + + rk::P4 p3; + rk::P4 p4; + p3 = p3_lab; + p4 = p4_lab; + + std::vector & secondaries = record.GetSecondaryParticleRecords(); + siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[lepton_index]; + siren::dataclasses::SecondaryParticleRecord & other = secondaries[other_index]; + + lepton.SetFourMomentum({p3.e(), p3.px(), p3.py(), p3.pz()}); + lepton.SetMass(p3.m()); + lepton.SetHelicity(record.primary_helicity); + other.SetFourMomentum({p4.e(), p4.px(), p4.py(), p4.pz()}); + other.SetMass(p4.m()); + other.SetHelicity(record.target_helicity); +} + +double CharmDISFromSpline::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { + double dxs = DifferentialCrossSection(interaction); + // if (dxs == 0) { + // std::cout << "diff xsec gives 0" << std::endl; + // } + double txs = TotalCrossSection(interaction); + if(dxs == 0) { + return 0.0; + } else { + // if (txs == 0) {std::cout << "wtf??? txs is 0 in final state prob" << txs << std::endl;} + // if (std::isinf(dxs)) {std::cout << "dxs is inf in final state prob" << std::endl;} + return dxs / txs; + } +} + +std::vector CharmDISFromSpline::GetPossiblePrimaries() const { + return std::vector(primary_types_.begin(), primary_types_.end()); +} + +std::vector CharmDISFromSpline::GetPossibleTargetsFromPrimary(siren::dataclasses::ParticleType primary_type) const { + return std::vector(target_types_.begin(), target_types_.end()); +} + +std::vector CharmDISFromSpline::GetPossibleSignatures() const { + return std::vector(signatures_.begin(), signatures_.end()); +} + +std::vector CharmDISFromSpline::GetPossibleTargets() const { + return std::vector(target_types_.begin(), target_types_.end()); +} + +std::vector CharmDISFromSpline::GetPossibleSignaturesFromParents(siren::dataclasses::ParticleType primary_type, siren::dataclasses::ParticleType target_type) const { + std::pair key(primary_type, target_type); + if(signatures_by_parent_types_.find(key) != signatures_by_parent_types_.end()) { + return signatures_by_parent_types_.at(key); + } else { + return std::vector(); + } +} + +std::vector CharmDISFromSpline::DensityVariables() const { + return std::vector{"Bjorken x", "Bjorken y"}; +} + +} // namespace interactions +} // namespace siren diff --git a/projects/interactions/private/pybindings/CharmDISFromSpline.h b/projects/interactions/private/pybindings/CharmDISFromSpline.h index 486556be4..e7349dc96 100644 --- a/projects/interactions/private/pybindings/CharmDISFromSpline.h +++ b/projects/interactions/private/pybindings/CharmDISFromSpline.h @@ -72,6 +72,7 @@ void register_CharmDISFromSpline(pybind11::module_ & m) { arg("target_types"), arg("units") = std::string("cm")) .def(self == self) + .def("SetInteractionType",&CharmDISFromSpline::SetInteractionType) .def("TotalCrossSection",overload_cast(&CharmDISFromSpline::TotalCrossSection, const_)) .def("TotalCrossSection",overload_cast(&CharmDISFromSpline::TotalCrossSection, const_)) .def("DifferentialCrossSection",overload_cast(&CharmDISFromSpline::DifferentialCrossSection, const_)) diff --git a/projects/interactions/public/SIREN/interactions/CharmDISFromSpline.h b/projects/interactions/public/SIREN/interactions/CharmDISFromSpline.h index 80f97a386..102ce4725 100644 --- a/projects/interactions/public/SIREN/interactions/CharmDISFromSpline.h +++ b/projects/interactions/public/SIREN/interactions/CharmDISFromSpline.h @@ -62,6 +62,10 @@ friend cereal::access; CharmDISFromSpline(std::string differential_filename, std::string total_filename, std::vector primary_types, std::vector target_types, std::string units = "cm"); void SetUnits(std::string units); + // this might be integrated later? could also make another initialization method + // problem with current implementation is that EM is not supported b/c at initialization we assume int = 1 + // this sets the isoscalar target mass + void SetInteractionType(int interaction); virtual bool equal(CrossSection const & other) const override; diff --git a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h new file mode 100644 index 000000000..62dc88a7d --- /dev/null +++ b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h @@ -0,0 +1,166 @@ +#pragma once +#ifndef SIREN_QuarkDISFromSpline_H +#define SIREN_QuarkDISFromSpline_H + +#include // for set +#include // for map +#include +#include // for vector +#include // for uint32_t +#include // for pair +#include +#include // for runtime... + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "SIREN/interactions/CrossSection.h" // for CrossSe... +#include "SIREN/dataclasses/InteractionSignature.h" // for Interac... +#include "SIREN/dataclasses/Particle.h" // for Particle + +namespace siren { namespace dataclasses { class InteractionRecord; } } +namespace siren { namespace utilities { class SIREN_random; } } + +namespace siren { +namespace interactions { + +class QuarkDISFromSpline : public CrossSection { +friend cereal::access; +private: + photospline::splinetable<> differential_cross_section_; + photospline::splinetable<> total_cross_section_; + + std::vector signatures_; + std::set primary_types_; + std::set target_types_; + std::map> targets_by_primary_types_; + std::map, std::vector> signatures_by_parent_types_; + + int interaction_type_; + double target_mass_; + double minimum_Q2_; + + double unit; + +public: + QuarkDISFromSpline(); + QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minumum_Q2, std::set primary_types, std::set target_types, std::string units = "cm"); + QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minumum_Q2, std::vector primary_types, std::vector target_types, std::string units = "cm"); + QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minumum_Q2, std::set primary_types, std::set target_types, std::string units = "cm"); + QuarkDISFromSpline(std::string differential_filename, std::string total_filename, std::set primary_types, std::set target_types, std::string units = "cm"); + QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minumum_Q2, std::vector primary_types, std::vector target_types, std::string units = "cm"); + QuarkDISFromSpline(std::string differential_filename, std::string total_filename, std::vector primary_types, std::vector target_types, std::string units = "cm"); + + void SetUnits(std::string units); + // this might be integrated later? could also make another initialization method + // problem with current implementation is that EM is not supported b/c at initialization we assume int = 1 + // this sets the isoscalar target mass + void SetInteractionType(int interaction); + + virtual bool equal(CrossSection const & other) const override; + + double TotalCrossSection(dataclasses::InteractionRecord const &) const override; + double TotalCrossSection(siren::dataclasses::ParticleType primary, double energy) const; + double DifferentialCrossSection(dataclasses::InteractionRecord const &) const override; + double DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2=std::numeric_limits::quiet_NaN()) const; + double InteractionThreshold(dataclasses::InteractionRecord const &) const override; + void SampleFinalState(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr random) const override; + + std::vector GetPossibleTargets() const override; + std::vector GetPossibleTargetsFromPrimary(siren::dataclasses::ParticleType primary_type) const override; + std::vector GetPossiblePrimaries() const override; + std::vector GetPossibleSignatures() const override; + std::vector GetPossibleSignaturesFromParents(siren::dataclasses::ParticleType primary_type, siren::dataclasses::ParticleType target_type) const override; + + virtual double FinalStateProbability(dataclasses::InteractionRecord const & record) const override; + + void LoadFromFile(std::string differential_filename, std::string total_filename); + void LoadFromMemory(std::vector & differential_data, std::vector & total_data); + + double GetMinimumQ2() const {return minimum_Q2_;}; + double GetTargetMass() const {return target_mass_;}; + int GetInteractionType() const {return interaction_type_;}; + + static double GetLeptonMass(siren::dataclasses::ParticleType lepton_type); + +public: + virtual std::vector DensityVariables() const override; + template + void save(Archive & archive, std::uint32_t const version) const { + if(version == 0) { + splinetable_buffer buf; + buf.size = 0; + auto result_obj = differential_cross_section_.write_fits_mem(); + buf.data = result_obj.first; + buf.size = result_obj.second; + + std::vector diff_blob; + diff_blob.resize(buf.size); + std::copy((char*)buf.data, (char*)buf.data + buf.size, &diff_blob[0]); + + archive(::cereal::make_nvp("DifferentialCrossSectionSpline", diff_blob)); + + buf.size = 0; + result_obj = total_cross_section_.write_fits_mem(); + buf.data = result_obj.first; + buf.size = result_obj.second; + + std::vector total_blob; + total_blob.resize(buf.size); + std::copy((char*)buf.data, (char*)buf.data + buf.size, &total_blob[0]); + + archive(::cereal::make_nvp("TotalCrossSectionSpline", total_blob)); + archive(::cereal::make_nvp("PrimaryTypes", primary_types_)); + archive(::cereal::make_nvp("TargetTypes", target_types_)); + archive(::cereal::make_nvp("InteractionType", interaction_type_)); + archive(::cereal::make_nvp("TargetMass", target_mass_)); + archive(::cereal::make_nvp("MinimumQ2", minimum_Q2_)); + archive(::cereal::make_nvp("Unit", unit)); + archive(cereal::virtual_base_class(this)); + } else { + throw std::runtime_error("QuarkDISFromSpline only supports version <= 0!"); + } + } + template + void load(Archive & archive, std::uint32_t version) { + if(version == 0) { + std::vector differential_data; + std::vector total_data; + archive(::cereal::make_nvp("DifferentialCrossSectionSpline", differential_data)); + archive(::cereal::make_nvp("TotalCrossSectionSpline", total_data)); + archive(::cereal::make_nvp("PrimaryTypes", primary_types_)); + archive(::cereal::make_nvp("TargetTypes", target_types_)); + archive(::cereal::make_nvp("InteractionType", interaction_type_)); + archive(::cereal::make_nvp("TargetMass", target_mass_)); + archive(::cereal::make_nvp("MinimumQ2", minimum_Q2_)); + archive(::cereal::make_nvp("Unit", unit)); + archive(cereal::virtual_base_class(this)); + LoadFromMemory(differential_data, total_data); + InitializeSignatures(); + } else { + throw std::runtime_error("QuarkDISFromSpline only supports version <= 0!"); + } + } +private: + void ReadParamsFromSplineTable(); + void InitializeSignatures(); +}; + +} // namespace interactions +} // namespace siren + +CEREAL_CLASS_VERSION(siren::interactions::QuarkDISFromSpline, 0); +CEREAL_REGISTER_TYPE(siren::interactions::QuarkDISFromSpline); +CEREAL_REGISTER_POLYMORPHIC_RELATION(siren::interactions::CrossSection, siren::interactions::QuarkDISFromSpline); + +#endif // SIREN_QuarkDISFromSpline_H diff --git a/python/SIREN_Controller.py b/python/SIREN_Controller.py index f754a4598..7daf7721a 100644 --- a/python/SIREN_Controller.py +++ b/python/SIREN_Controller.py @@ -480,6 +480,8 @@ def GenerateEvents(self, N=None, fill_tables_at_exit=True): prev_time = time.time() while (self.injector.InjectedEvents() < self.events_to_inject) and (count < N): print("Injecting Event %d/%d " % (count, N), end="\r") + # print("Injecting Event %d/%d " % (count, N)) # for debugging purposes + event = self.injector.GenerateEvent() self.events.append(event) t = time.time() @@ -513,6 +515,7 @@ def SaveEvents(self, filename, fill_tables_at_exit=True, "event_global_time":[], # global time of each event "num_interactions":[], # number of interactions per event "vertex":[], # vertex of each interaction in an event + "primary_initial_position":[], # initial position of primary in each interaction of an event "in_fiducial":[], # whether or not each vertex is in the fiducial volume "primary_type":[], # primary type of each interaction "target_type":[], # target type of each interaction @@ -524,6 +527,8 @@ def SaveEvents(self, filename, fill_tables_at_exit=True, } for ie, event in enumerate(self.events): print("Saving Event %d/%d " % (ie, len(self.events)), end="\r") + # print("Saving Event %d/%d " % (ie, len(self.events))) # for debugging purposes + t0 = time.time() datasets["event_weight"].append(self.weighter.EventWeight(event) if hasattr(self,"weighter") else 0) datasets["event_weight_time"].append(time.time()-t0) @@ -531,6 +536,7 @@ def SaveEvents(self, filename, fill_tables_at_exit=True, datasets["event_global_time"].append(self.global_times[ie]) # add empty lists for each per interaction dataset for k in ["vertex", + "primary_initial_position", "in_fiducial", "primary_type", "target_type", @@ -543,6 +549,7 @@ def SaveEvents(self, filename, fill_tables_at_exit=True, # loop over interactions for id, datum in enumerate(event.tree): datasets["vertex"][-1].append(np.array(datum.record.interaction_vertex,dtype=float)) + datasets["primary_initial_position"][-1].append(np.array(datum.record.primary_initial_position,dtype=float)) # primary particle stuff datasets["primary_type"][-1].append(int(datum.record.signature.primary_type)) From abf68839d41dd92b138fa66fad83a9abdec45bd8 Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Tue, 19 Nov 2024 15:41:19 -0500 Subject: [PATCH 10/93] new stuff --- .../dataclasses/private/InteractionRecord.cxx | 20 + projects/dataclasses/private/Particle.cxx | 6 +- .../public/SIREN/dataclasses/Particle.h | 1 + projects/injection/private/Injector.cxx | 16 +- projects/interactions/CMakeLists.txt | 2 + .../private/CharmDISFromSpline.cxx | 2 + .../private/QuarkDISFromSpline.cxx | 367 ++++++++++++++---- .../private/pybindings/QuarkDISFromSpline.h | 90 +++++ .../private/pybindings/interactions.cxx | 3 + .../SIREN/interactions/QuarkDISFromSpline.h | 32 +- python/SIREN_Controller.py | 1 + 11 files changed, 458 insertions(+), 82 deletions(-) create mode 100644 projects/interactions/private/pybindings/QuarkDISFromSpline.h diff --git a/projects/dataclasses/private/InteractionRecord.cxx b/projects/dataclasses/private/InteractionRecord.cxx index 48c52530b..4b79b75b8 100644 --- a/projects/dataclasses/private/InteractionRecord.cxx +++ b/projects/dataclasses/private/InteractionRecord.cxx @@ -327,13 +327,22 @@ void PrimaryDistributionRecord::FinalizeAvailable(InteractionRecord & record) co } void PrimaryDistributionRecord::Finalize(InteractionRecord & record) const { + //std::cout << "starting finalize" << std::endl; + //std::cout << record.signature << std::endl; + //std::cout << record.primary_momentum[0] << std::endl; + record.signature.primary_type = type; record.primary_id = GetID(); record.interaction_vertex = GetInteractionVertex(); record.primary_initial_position = GetInitialPosition(); + //std::cout << "in primary record finalize" << std::endl; record.primary_mass = GetMass(); record.primary_momentum = GetFourMomentum(); record.primary_helicity = GetHelicity(); + //std::cout << "finished primary record finalize" << std::endl; + + //std::cout << record.signature << std::endl; + //std::cout << record.primary_momentum[0] << std::endl; } ///////////////////////////////////////// @@ -553,11 +562,16 @@ void SecondaryParticleRecord::UpdateMomentum() const { } void SecondaryParticleRecord::Finalize(InteractionRecord & record) const { + ////std::cout << "SecPartRecord::Finalize : starting for " << type << std::endl; + assert(record.signature.secondary_types.at(secondary_index) == type); record.secondary_ids.at(secondary_index) = GetID(); record.secondary_masses.at(secondary_index) = GetMass(); record.secondary_momenta.at(secondary_index) = GetFourMomentum(); record.secondary_helicities.at(secondary_index) = GetHelicity(); + ////std::cout << "SecPartRecord::Finalize : finished for " << type << " with" << std::endl; + ////std::cout << record << std::endl; + } ///////////////////////////////////////// @@ -676,6 +690,11 @@ SecondaryParticleRecord const & CrossSectionDistributionRecord::GetSecondaryPart } void CrossSectionDistributionRecord::Finalize(InteractionRecord & record) const { + //std::cout << "XsecDistRecord::Finalize : starting" << std::endl; + //std::cout << record.signature << std::endl; + //std::cout << signature << std::endl; + //std::cout << primary_momentum[0] << std::endl; + record.target_id = target_id; record.target_mass = target_mass; record.target_helicity = target_helicity; @@ -688,6 +707,7 @@ void CrossSectionDistributionRecord::Finalize(InteractionRecord & record) const record.secondary_helicities.resize(secondary_particles.size()); for(SecondaryParticleRecord const & secondary : secondary_particles) { + //std::cout << "XsecDistRecord::Finalize : going to secondaries: " << secondary << std::endl; secondary.Finalize(record); } } diff --git a/projects/dataclasses/private/Particle.cxx b/projects/dataclasses/private/Particle.cxx index 7952adc1e..afc9e559f 100644 --- a/projects/dataclasses/private/Particle.cxx +++ b/projects/dataclasses/private/Particle.cxx @@ -93,6 +93,10 @@ bool isHadron(Particle::ParticleType p){ p==Particle::ParticleType::DPlus || p==Particle::ParticleType::DMinus); } - +bool isD(Particle::ParticleType p){ + return(p==Particle::ParticleType::D0 || p==Particle::ParticleType::D0Bar || + p==Particle::ParticleType::DPlus || p==Particle::ParticleType::DMinus); + } + } // namespace utilities } // namespace siren diff --git a/projects/dataclasses/public/SIREN/dataclasses/Particle.h b/projects/dataclasses/public/SIREN/dataclasses/Particle.h index fff79c4a6..a272b8c58 100644 --- a/projects/dataclasses/public/SIREN/dataclasses/Particle.h +++ b/projects/dataclasses/public/SIREN/dataclasses/Particle.h @@ -76,6 +76,7 @@ bool isCharged(ParticleType p); bool isNeutrino(ParticleType p); bool isQuark(Particle::ParticleType p); bool isHadron(Particle::ParticleType p); +bool isD(Particle::ParticleType p); } // namespace dataclasses diff --git a/projects/injection/private/Injector.cxx b/projects/injection/private/Injector.cxx index 7ca32979c..197da2c84 100644 --- a/projects/injection/private/Injector.cxx +++ b/projects/injection/private/Injector.cxx @@ -159,7 +159,7 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record throw(siren::utilities::InjectionFailure("No particle interaction!")); } - //std::cout << "in sample cross section" << std::endl; + ////std::cout << "in sample cross section" << std::endl; std::set const & possible_targets = interactions->TargetTypes(); @@ -267,6 +267,7 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record } } } + //std::cout << "injector :: sample cross sections: after obtaining signatures" << std::endl; if(total_prob == 0) throw(siren::utilities::InjectionFailure("No valid interactions for this event!")); // Throw a random number @@ -287,12 +288,17 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record record.target_mass = detector_model->GetTargetMass(record.signature.target_type); siren::dataclasses::CrossSectionDistributionRecord xsec_record(record); if(r <= xsec_prob) { - //std::cout << "going into sampel final state" << std::endl; + //std::cout << "injector::sample cross section: going into sampel final state" << std::endl; matching_cross_sections[index]->SampleFinalState(xsec_record, random); + //std::cout << "injector::sample cross section: finished sampling" << std::endl; + } else { matching_decays[index - matching_cross_sections.size()]->SampleFinalState(xsec_record, random); } + ////std::cout << "injector::sample cross section: calling finalizing" << std::endl; xsec_record.Finalize(record); + ////std::cout << "injector::sample cross section: finished finalizing" << std::endl; + } } @@ -341,13 +347,15 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { // Initial Process while(true) { tries += 1; + ////std::cout << "injector::GenerateEvent : trying to generate with trial number " << tries << std::endl; try { - //std::cout << "generating primary process" << std::endl; + ////std::cout << "injector::GenerateEvent : generating primary process" << std::endl; siren::dataclasses::PrimaryDistributionRecord primary_record(primary_process->GetPrimaryType()); for(auto & distribution : primary_process->GetPrimaryInjectionDistributions()) { distribution->Sample(random, detector_model, primary_process->GetInteractions(), primary_record); } primary_record.Finalize(record); + //std::cout << "injector::GenerateEvent : sampling cross section" << std::endl; SampleCrossSection(record); break; } catch(siren::utilities::InjectionFailure const & e) { @@ -367,6 +375,7 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { std::shared_ptr parent = tree.add_entry(record); // Secondary Processes + //std::cout << "injector::GenerateEvent : sampling secondary process" << std::endl; std::deque, std::shared_ptr>> secondaries; std::function)> add_secondaries = [&](std::shared_ptr parent) { for(size_t i=0; irecord.signature.secondary_types.size(); ++i) { @@ -399,6 +408,7 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { } } + //std::cout << "finished sampling secondary process" << std::endl; injected_events += 1; return tree; } diff --git a/projects/interactions/CMakeLists.txt b/projects/interactions/CMakeLists.txt index a8eec569d..ddd9936c8 100644 --- a/projects/interactions/CMakeLists.txt +++ b/projects/interactions/CMakeLists.txt @@ -21,6 +21,8 @@ LIST (APPEND interactions_SOURCES ${PROJECT_SOURCE_DIR}/projects/interactions/private/CharmHadronization.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/CharmMesonDecay.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/DMesonELoss.cxx + ${PROJECT_SOURCE_DIR}/projects/interactions/private/QuarkDISFromSpline.cxx + ) add_library(SIREN_interactions OBJECT ${interactions_SOURCES}) set_property(TARGET SIREN_interactions PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/projects/interactions/private/CharmDISFromSpline.cxx b/projects/interactions/private/CharmDISFromSpline.cxx index 9844231ed..27921cb33 100644 --- a/projects/interactions/private/CharmDISFromSpline.cxx +++ b/projects/interactions/private/CharmDISFromSpline.cxx @@ -45,6 +45,8 @@ namespace { bool kinematicallyAllowed(double x, double y, double E, double M, double m) { if(x > 1) //Eq. 6 right inequality return false; + // this is to get rid of the infinities as a temporary solution + if (x < 1e-6 or y < 1e-6) return false; if(x < ((m * m) / (2 * M * (E - m)))) //Eq. 6 left inequality return false; //denominator of a and b diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 9844231ed..0c1842e0c 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -1,4 +1,4 @@ -#include "SIREN/interactions/CharmDISFromSpline.h" +#include "SIREN/interactions/QuarkDISFromSpline.h" #include // for map, opera... #include // for set, opera... @@ -27,6 +27,8 @@ #include "SIREN/dataclasses/Particle.h" // for Particle #include "SIREN/utilities/Random.h" // for SIREN_random #include "SIREN/utilities/Constants.h" // for electronMass +#include "SIREN/utilities/Errors.h" // for PythonImplementationError + namespace siren { namespace interactions { @@ -47,6 +49,8 @@ bool kinematicallyAllowed(double x, double y, double E, double M, double m) { return false; if(x < ((m * m) / (2 * M * (E - m)))) //Eq. 6 left inequality return false; + if (x < 1e-6 || y < 1e-6) return false; + //denominator of a and b double d = 2 * (1 + (M * x) / (2 * E)); //the numerator of a (or a*d) @@ -62,47 +66,63 @@ bool kinematicallyAllowed(double x, double y, double E, double M, double m) { } } -CharmDISFromSpline::CharmDISFromSpline() {} +QuarkDISFromSpline::QuarkDISFromSpline() { + // initialize the pdf normalization and cdf table for the hadronization process + normalize_pdf(); + compute_cdf(); +} -CharmDISFromSpline::CharmDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { +QuarkDISFromSpline::QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { + normalize_pdf(); + compute_cdf(); LoadFromMemory(differential_data, total_data); InitializeSignatures(); SetUnits(units); } -CharmDISFromSpline::CharmDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { +QuarkDISFromSpline::QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { + normalize_pdf(); + compute_cdf(); LoadFromMemory(differential_data, total_data); InitializeSignatures(); SetUnits(units); } -CharmDISFromSpline::CharmDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { +QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { + normalize_pdf(); + compute_cdf(); LoadFromFile(differential_filename, total_filename); InitializeSignatures(); SetUnits(units); } -CharmDISFromSpline::CharmDISFromSpline(std::string differential_filename, std::string total_filename, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types) { +QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types) { + normalize_pdf(); + compute_cdf(); LoadFromFile(differential_filename, total_filename); ReadParamsFromSplineTable(); InitializeSignatures(); SetUnits(units); } -CharmDISFromSpline::CharmDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { +QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { + normalize_pdf(); + compute_cdf(); LoadFromFile(differential_filename, total_filename); InitializeSignatures(); SetUnits(units); } -CharmDISFromSpline::CharmDISFromSpline(std::string differential_filename, std::string total_filename, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()) { +QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()) { + normalize_pdf(); + compute_cdf(); LoadFromFile(differential_filename, total_filename); ReadParamsFromSplineTable(); InitializeSignatures(); SetUnits(units); } -void CharmDISFromSpline::SetUnits(std::string units) { +void QuarkDISFromSpline::SetUnits(std::string units) { std::transform(units.begin(), units.end(), units.begin(), [](unsigned char c){ return std::tolower(c); }); if(units == "cm") { @@ -114,13 +134,17 @@ void CharmDISFromSpline::SetUnits(std::string units) { } } -void CharmDISFromSpline::SetInteractionType(int interaction) { +void QuarkDISFromSpline::SetInteractionType(int interaction) { interaction_type_ = interaction; } -bool CharmDISFromSpline::equal(CrossSection const & other) const { - const CharmDISFromSpline* x = dynamic_cast(&other); +void QuarkDISFromSpline::SetQuarkType(int q_type) { + quark_type_ = q_type; +} +bool QuarkDISFromSpline::equal(CrossSection const & other) const { + const QuarkDISFromSpline* x = dynamic_cast(&other); + // to do: include more features in the hadronization side to check equivalence if(!x) return false; else @@ -146,7 +170,7 @@ bool CharmDISFromSpline::equal(CrossSection const & other) const { x->total_cross_section_); } -void CharmDISFromSpline::LoadFromFile(std::string dd_crossSectionFile, std::string total_crossSectionFile) { +void QuarkDISFromSpline::LoadFromFile(std::string dd_crossSectionFile, std::string total_crossSectionFile) { differential_cross_section_ = photospline::splinetable<>(dd_crossSectionFile.c_str()); @@ -161,12 +185,12 @@ void CharmDISFromSpline::LoadFromFile(std::string dd_crossSectionFile, std::stri + " dimensions, should have 1, log10(E)"); } -void CharmDISFromSpline::LoadFromMemory(std::vector & differential_data, std::vector & total_data) { +void QuarkDISFromSpline::LoadFromMemory(std::vector & differential_data, std::vector & total_data) { differential_cross_section_.read_fits_mem(differential_data.data(), differential_data.size()); total_cross_section_.read_fits_mem(total_data.data(), total_data.size()); } -double CharmDISFromSpline::GetLeptonMass(siren::dataclasses::ParticleType lepton_type) { +double QuarkDISFromSpline::GetLeptonMass(siren::dataclasses::ParticleType lepton_type) { int32_t lepton_number = std::abs(static_cast(lepton_type)); double lepton_mass; switch(lepton_number) { @@ -192,7 +216,45 @@ double CharmDISFromSpline::GetLeptonMass(siren::dataclasses::ParticleType lepton return lepton_mass; } -void CharmDISFromSpline::ReadParamsFromSplineTable() { +double QuarkDISFromSpline::getHadronMass(siren::dataclasses::ParticleType hadron_type) { + switch(hadron_type){ + case siren::dataclasses::ParticleType::D0: + return( siren::utilities::Constants::D0Mass); + case siren::dataclasses::ParticleType::D0Bar: + return( siren::utilities::Constants::D0Mass); + case siren::dataclasses::ParticleType::DPlus: + return( siren::utilities::Constants::DPlusMass); + case siren::dataclasses::ParticleType::DMinus: + return( siren::utilities::Constants::DPlusMass); + case siren::dataclasses::ParticleType::Charm: + return( siren::utilities::Constants::CharmMass); + case siren::dataclasses::ParticleType::CharmBar: + return( siren::utilities::Constants::CharmMass); + default: + return(0.0); + } +} + + +std::map QuarkDISFromSpline::getIndices(siren::dataclasses::InteractionSignature signature) { + int lepton_id, hadron_id, meson_id; + for (size_t i = 0; i < signature.secondary_types.size(); i++){ + if (isLepton(signature.secondary_types[i])) { + lepton_id = i; + continue; + } else if (isD(signature.secondary_types[i])) { + meson_id = i; + continue; + } else { + hadron_id = i; + continue; + } + } + return {{"lepton", lepton_id}, {"hadron", hadron_id}, {"meson", meson_id}}; +} + + +void QuarkDISFromSpline::ReadParamsFromSplineTable() { // returns true if successfully read target mass bool mass_good = differential_cross_section_.read_key("TARGETMASS", target_mass_); if (mass_good) {std::cout << "read target mass!!" << std::endl;} // for debugging purposes @@ -200,19 +262,24 @@ void CharmDISFromSpline::ReadParamsFromSplineTable() { bool int_good = differential_cross_section_.read_key("INTERACTION", interaction_type_); // returns true if successfully read minimum Q2 bool q2_good = differential_cross_section_.read_key("Q2MIN", minimum_Q2_); + // returns true if successfully read quark type + bool qtype_good = differential_cross_section_.read_key("QUARKTYPE", quark_type_); + if(!int_good) { // assume DIS to preserve compatability with previous versions interaction_type_ = 1; } + if (!qtype_good) { + quark_type_ = 1; // assume quark is produced + } + if(!q2_good) { // assume 1 GeV^2 minimum_Q2_ = 1; } - std::cout << "Q2 good status is " << q2_good << "and is set to " << minimum_Q2_; - if(!mass_good) { if(int_good) { if(interaction_type_ == 1 or interaction_type_ == 2) { @@ -235,23 +302,19 @@ void CharmDISFromSpline::ReadParamsFromSplineTable() { } } } - std::cout << "target mass is " << target_mass_ << std::endl; - } -void CharmDISFromSpline::InitializeSignatures() { +void QuarkDISFromSpline::InitializeSignatures() { signatures_.clear(); for(auto primary_type : primary_types_) { dataclasses::InteractionSignature signature; signature.primary_type = primary_type; - if(not isNeutrino(primary_type)) { throw std::runtime_error("This DIS implementation only supports neutrinos as primaries!"); } - + // first push back the charged lepton product siren::dataclasses::ParticleType charged_lepton_product = siren::dataclasses::ParticleType::unknown; siren::dataclasses::ParticleType neutral_lepton_product = primary_type; - if(primary_type == siren::dataclasses::ParticleType::NuE) { charged_lepton_product = siren::dataclasses::ParticleType::EMinus; } else if(primary_type == siren::dataclasses::ParticleType::NuEBar) { @@ -267,7 +330,6 @@ void CharmDISFromSpline::InitializeSignatures() { } else { throw std::runtime_error("InitializeSignatures: Unkown parent neutrino type!"); } - if(interaction_type_ == 1) { signature.secondary_types.push_back(charged_lepton_product); } else if(interaction_type_ == 2) { @@ -277,20 +339,100 @@ void CharmDISFromSpline::InitializeSignatures() { } else { throw std::runtime_error("InitializeSignatures: Unkown interaction type!"); } + // now push back the hadron product + signature.secondary_types.push_back(siren::dataclasses::ParticleType::Hadrons); + // define the charmed meson types based on the quark type, now considering only D0 and D+ + if (quark_type_ == 1) { + D_types_ = {siren::dataclasses::Particle::ParticleType::D0, + siren::dataclasses::Particle::ParticleType::DPlus}; + } else { + D_types_ = {siren::dataclasses::Particle::ParticleType::D0Bar, + siren::dataclasses::Particle::ParticleType::DMinus}; + } + // push back the meson type + for (auto meson_type : D_types_) { + dataclasses::InteractionSignature full_signature = signature; + full_signature.secondary_types.push_back(meson_type); + // and finally set the target type and push back the entire signature as well as sig by target + for(auto target_type : target_types_) { + full_signature.target_type = target_type; + + signatures_.push_back(full_signature); + + std::pair key(primary_type, target_type); + signatures_by_parent_types_[key].push_back(full_signature); + } + } + } +} - signature.secondary_types.push_back(siren::dataclasses::ParticleType::Charm); - for(auto target_type : target_types_) { - signature.target_type = target_type; +void QuarkDISFromSpline::normalize_pdf() { + if (fragmentation_integral == 0){ + std::function integrand = [&] (double x) -> double { + return (0.8 / x ) / (std::pow(1 - (1 / x) - (0.2 / (1 - x)), 2)); + }; + fragmentation_integral = siren::utilities::rombergIntegrate(integrand, 0.001, 0.999); + } else { + std::cout << "Something is wrong... you already computed the normalization" << std::endl; + return; + } +} - signatures_.push_back(signature); +void QuarkDISFromSpline::compute_cdf() { + // first set the z nodes + std::vector zspline; + for (int i = 0; i < 100; ++i) { + zspline.push_back(0.01 + i * (0.99-0.01) / 100 ); + } - std::pair key(primary_type, target_type); - signatures_by_parent_types_[key].push_back(signature); + // declare the cdf vectors + std::vector cdf_vector; + std::vector cdf_z_nodes; + std::vector pdf_vector; + + cdf_z_nodes.push_back(0); + cdf_vector.push_back(0); + pdf_vector.push_back(0); + + // compute the spline table + for (int i = 0; i < zspline.size(); ++i) { + if (i == 0) { + double cur_z = zspline[i]; + double cur_pdf = sample_pdf(cur_z); + double area = cur_z * cur_pdf * 0.5; + pdf_vector.push_back(cur_pdf); + cdf_vector.push_back(area); + cdf_z_nodes.push_back(cur_z); + continue; } + double cur_z = zspline[i]; + double cur_pdf = sample_pdf(cur_z); + double area = 0.5 * (pdf_vector[i - 1] + cur_pdf) * (zspline[i] - zspline[i - 1]); + pdf_vector.push_back(cur_pdf); + cdf_z_nodes.push_back(cur_z); + cdf_vector.push_back(area + cdf_vector.back()); } + + cdf_z_nodes.push_back(1); + cdf_vector.push_back(1); + pdf_vector.push_back(0); + + + // set the spline table + siren::utilities::TableData1D inverse_cdf_data; + inverse_cdf_data.x = cdf_vector; + inverse_cdf_data.f = cdf_z_nodes; + + inverseCdfTable = siren::utilities::Interpolator1D(inverse_cdf_data); + + return; +} + +double QuarkDISFromSpline::sample_pdf(double x) const { + return (0.8 / x ) / (std::pow(1 - (1 / x) - (0.2 / (1 - x)), 2)) / fragmentation_integral; } -double CharmDISFromSpline::TotalCrossSection(dataclasses::InteractionRecord const & interaction) const { +double QuarkDISFromSpline::TotalCrossSection(dataclasses::InteractionRecord const & interaction) const { siren::dataclasses::ParticleType primary_type = interaction.signature.primary_type; rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); double primary_energy; @@ -303,7 +445,7 @@ double CharmDISFromSpline::TotalCrossSection(dataclasses::InteractionRecord cons return TotalCrossSection(primary_type, primary_energy); } -double CharmDISFromSpline::TotalCrossSection(siren::dataclasses::ParticleType primary_type, double primary_energy) const { +double QuarkDISFromSpline::TotalCrossSection(siren::dataclasses::ParticleType primary_type, double primary_energy) const { if(not primary_types_.count(primary_type)) { throw std::runtime_error("Supplied primary not supported by cross section!"); } @@ -328,34 +470,38 @@ double CharmDISFromSpline::TotalCrossSection(siren::dataclasses::ParticleType pr return unit * std::pow(10.0, log_xs); } -double CharmDISFromSpline::DifferentialCrossSection(dataclasses::InteractionRecord const & interaction) const { +double QuarkDISFromSpline::DifferentialCrossSection(dataclasses::InteractionRecord const & interaction) const { rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); rk::P4 p2(geom3::Vector3(0, 0, 0), interaction.target_mass); double primary_energy; primary_energy = interaction.primary_momentum[0]; - assert(interaction.signature.secondary_types.size() == 2); - unsigned int lepton_index = (isLepton(interaction.signature.secondary_types[0])) ? 0 : 1; - unsigned int other_index = 1 - lepton_index; + assert(interaction.signature.secondary_types.size() == 3); + std::map secondaries = getIndices(interaction.signature); + unsigned int lepton_index = secondaries["lepton"]; + unsigned int hadron_index = secondaries["hadron"]; + unsigned int meson_index = secondaries["meson"]; std::array const & mom3 = interaction.secondary_momenta[lepton_index]; - std::array const & mom4 = interaction.secondary_momenta[other_index]; - rk::P4 p3(geom3::Vector3(mom3[1], mom3[2], mom3[3]), interaction.secondary_masses[lepton_index]); - rk::P4 p4(geom3::Vector3(mom4[1], mom4[2], mom4[3]), interaction.secondary_masses[other_index]); + std::array const & mom_x = interaction.secondary_momenta[hadron_index]; + std::array const & mom_d = interaction.secondary_momenta[meson_index]; + rk::P4 p3(geom3::Vector3(mom3[1], mom3[2], mom3[3]), interaction.secondary_masses[lepton_index]); + rk::P4 p_x(geom3::Vector3(mom_x[1], mom_x[2], mom_x[3]), interaction.secondary_masses[hadron_index]); + rk::P4 p_d(geom3::Vector3(mom_d[1], mom_d[2], mom_d[3]), interaction.secondary_masses[meson_index]); + rk::P4 p4 = p_x + p_d; // this assume that we are working in a good frame where the hadronization vertex has 4-momentum conserved rk::P4 q = p1 - p3; + // however p4 is not used in computation here so we should be fine... double Q2 = -q.dot(q); double x, y; double lepton_mass = GetLeptonMass(interaction.signature.secondary_types[lepton_index]); - y = 1.0 - p2.dot(p3) / p2.dot(p1); x = Q2 / (2.0 * p2.dot(q)); double log_energy = log10(primary_energy); std::array coordinates{{log_energy, log10(x), log10(y)}}; std::array centers; - if (Q2 < minimum_Q2_ || !kinematicallyAllowed(x, y, primary_energy, target_mass_, lepton_mass) || !differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { // std::cout << "weighting: revert back to saved x and y" << std::endl; @@ -366,11 +512,9 @@ double CharmDISFromSpline::DifferentialCrossSection(dataclasses::InteractionReco Q2 = 2. * E1_lab * E2_lab * x * y; } return DifferentialCrossSection(primary_energy, x, y, lepton_mass, Q2); - - } -double CharmDISFromSpline::DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2) const { +double QuarkDISFromSpline::DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2) const { double log_energy = log10(energy); // check preconditions if(log_energy < differential_cross_section_.lower_extent(0) @@ -386,9 +530,6 @@ double CharmDISFromSpline::DifferentialCrossSection(double energy, double x, dou return 0.0; } - // we assume that: - // the target is stationary so its energy is just its mass - // the incoming neutrino is massless, so its kinetic energy is its total energy if(std::isnan(Q2)) { Q2 = 2.0 * energy * target_mass_ * x * y; } @@ -413,22 +554,29 @@ double CharmDISFromSpline::DifferentialCrossSection(double energy, double x, dou std::cout << "energy, x, y, Q2 are " << energy << " " << x << " " << y << " " << Q2 << " " << std::endl; std::cout << "spline value read is " << differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0) << std::endl; } - return unit * result; } -double CharmDISFromSpline::InteractionThreshold(dataclasses::InteractionRecord const & interaction) const { +double QuarkDISFromSpline::InteractionThreshold(dataclasses::InteractionRecord const & interaction) const { // Consider implementing DIS thershold at some point return 0; } -void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { +void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { + // first obtain the indices from secondaries + // std::cout << "in sample final state" << std::endl; + std::map secondary_indices = getIndices(record.signature); + unsigned int lepton_index = secondary_indices["lepton"]; + unsigned int hadron_index = secondary_indices["hadron"]; + unsigned int meson_index = secondary_indices["meson"]; + // Uses Metropolis-Hastings Algorithm! // useful for cases where we don't know the supremum of our distribution, and the distribution is multi-dimensional if (differential_cross_section_.get_ndim() != 3) { throw std::runtime_error("I expected 3 dimensions in the cross section spline, but got " + std::to_string(differential_cross_section_.get_ndim()) +". Maybe your fits file doesn't have the right 'INTERACTION' key?"); } rk::P4 p1(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); + // std::cout << "quark::sampleFinalState : primary momentum is read to be " << p1 << std::endl; rk::P4 p2(geom3::Vector3(0, 0, 0), record.target_mass); // we assume that: @@ -444,8 +592,7 @@ void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR p2_lab = p2; primary_energy = p1_lab.e(); - unsigned int lepton_index = (isLepton(record.signature.secondary_types[0])) ? 0 : 1; - unsigned int other_index = 1 - lepton_index; + // correctly assign lepton, hadron and meson index double m = GetLeptonMass(record.signature.secondary_types[lepton_index]); double m1 = record.primary_mass; @@ -566,9 +713,8 @@ void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR // std::cout << "trial Q is" << trialQ << std::endl; } } - //////////////////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////////////////// + + // scaling down to handle numerical issues double final_x = pow(10., kin_vars[1]); double final_y = pow(10., kin_vars[2]); record.interaction_parameters.clear(); @@ -646,53 +792,132 @@ void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR rk::P4 p3; rk::P4 p4; - p3 = p3_lab; - p4 = p4_lab; - + p3 = p3_lab; // now we have our lepton momentum set, which should not be modified from here on + p4 = p4_lab; // momentum of the virtual charm + // std::cout << "charm momentum is " << p4 << std::endl; + + // compute the energy and 3-momentum of the virtual charm + // std::cout << "the virtual charm off-shell mass is " << p4.m() << std::endl; + double p3c = std::sqrt(std::pow(p4.px(), 2) + std::pow(p4.py(), 2) + std::pow(p4.pz(), 2)); + double Ec = p4.e(); //energy of primary charm + double mCH = getHadronMass(record.signature.secondary_types[meson_index]); // obtain charmed hadron mass + + // accept-reject sampling for a valid momentum fragmentation + bool frag_accept; + double randValue; + double z; + double ECH; + + // add a maximum number of trials in the while loop + int max_sampling = 500; + int sampling = 0; + + // sample again if this eenrgy is not kinematically allowed + do { + sampling += 1; + if (sampling > max_sampling) { + std::cout << "energy of the charm is " << Ec << " and momentum is " << p3c << std::endl; + std::cout << "desired mass of hadron is " << mCH << std::endl; + // throw(siren::utilities::InjectionFailure("Failed to sample hadronization!")); + break; + } + randValue = random->Uniform(0,1); + z = inverseCdfTable(randValue); + ECH = z * Ec; + if (std::pow(ECH, 2) - std::pow(mCH, 2) <= 0) { + frag_accept = false; + } else { + frag_accept = true; + } + double test_ED = ECH; + double test_EX = (1-z) * Ec; + double test_p3D = std::sqrt(std::pow(test_ED, 2) - std::pow(mCH, 2)); + double test_rD = test_p3D / p3c; + double test_p3X = std::pow((1 - test_rD), 2) * std::pow(p3c, 2); + if (std::pow(test_EX, 2) - std::pow(test_p3X, 2) <= 0) {frag_accept = false;} else {frag_accept = true;} + } while (!frag_accept); + + // set the 3-momentum of the charmed meson and subsequently the 4-momentum + double p3CH = std::sqrt(std::pow(ECH, 2) - std::pow(mCH, 2)); //obtain charmed hadron 3-momentum + double rCH = p3CH/p3c; // ratio of momentum carried away by the charmed hadron, assume collinearity + rk::P4 p4CH(geom3::Vector3(rCH * record.primary_momentum[1], rCH * record.primary_momentum[2], rCH * record.primary_momentum[3]), mCH); + + // the 4 momentum (and the mass) of the resulting hadronic vertex is determined solely via 4-momentum conservation + rk::P4 p4X = p4 - p4CH; + // let's first assume massless hadron final state + // double EX = (1 - z) * Ec; // energy of the hadronic shower + // double p3X = EX; // assume no hadronic mass + // double rX = p3X/p3c; // assume collinear + // rk::P4 p4X(geom3::Vector3(rX * record.primary_momentum[1], rX * record.primary_momentum[2], rX * record.primary_momentum[3]), 0); + + // now we proceed to saving the final state kinematics std::vector & secondaries = record.GetSecondaryParticleRecords(); siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[lepton_index]; - siren::dataclasses::SecondaryParticleRecord & other = secondaries[other_index]; + siren::dataclasses::SecondaryParticleRecord & hadron = secondaries[hadron_index]; + siren::dataclasses::SecondaryParticleRecord & meson = secondaries[meson_index]; + // std::cout << "QuarkDIS::SampleFInalState : the indices are: " << lepton_index << hadron_index<< meson_index << std::endl; lepton.SetFourMomentum({p3.e(), p3.px(), p3.py(), p3.pz()}); + // std::cout << "setting lepton mass with lepton momentum " << p3 << std::endl; lepton.SetMass(p3.m()); lepton.SetHelicity(record.primary_helicity); - other.SetFourMomentum({p4.e(), p4.px(), p4.py(), p4.pz()}); - other.SetMass(p4.m()); - other.SetHelicity(record.target_helicity); + hadron.SetFourMomentum({p4X.e(), p4X.px(), p4X.py(), p4X.pz()}); + // std::cout << "setting hadron mass with hadron momentum " << p4X << std::endl; + hadron.SetMass(p4X.m()); + hadron.SetHelicity(record.target_helicity); + meson.SetFourMomentum({p4CH.e(), p4CH.px(), p4CH.py(), p4CH.pz()}); + // std::cout << "setting meson mass with meson momentum " << p4CH << std::endl; + meson.SetMass(p4CH.m()); + meson.SetHelicity(record.target_helicity); // this needs working on + // std::cout << "finished sampling final state" << std::endl; +} + +double QuarkDISFromSpline::FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const { + if (secondary == siren::dataclasses::Particle::ParticleType::D0 || secondary == siren::dataclasses::Particle::ParticleType::D0Bar) { + return 0.6; + } else if (secondary == siren::dataclasses::Particle::ParticleType::DPlus || secondary == siren::dataclasses::Particle::ParticleType::DMinus) { + return 0.23; + } // D_s and Lambda^+ not yet implemented + return 0; } -double CharmDISFromSpline::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { +double QuarkDISFromSpline::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { + // first compute the differential and total cross section double dxs = DifferentialCrossSection(interaction); // if (dxs == 0) { // std::cout << "diff xsec gives 0" << std::endl; // } double txs = TotalCrossSection(interaction); + //then compute the fragmentation probability + std::map secondaries = getIndices(interaction.signature); + unsigned int meson_index = secondaries["meson"]; + double fragfrac = FragmentationFraction(interaction.signature.secondary_types[meson_index]); if(dxs == 0) { return 0.0; } else { // if (txs == 0) {std::cout << "wtf??? txs is 0 in final state prob" << txs << std::endl;} // if (std::isinf(dxs)) {std::cout << "dxs is inf in final state prob" << std::endl;} - return dxs / txs; + return dxs / txs * fragfrac; } } -std::vector CharmDISFromSpline::GetPossiblePrimaries() const { +std::vector QuarkDISFromSpline::GetPossiblePrimaries() const { return std::vector(primary_types_.begin(), primary_types_.end()); } -std::vector CharmDISFromSpline::GetPossibleTargetsFromPrimary(siren::dataclasses::ParticleType primary_type) const { +std::vector QuarkDISFromSpline::GetPossibleTargetsFromPrimary(siren::dataclasses::ParticleType primary_type) const { return std::vector(target_types_.begin(), target_types_.end()); } -std::vector CharmDISFromSpline::GetPossibleSignatures() const { +std::vector QuarkDISFromSpline::GetPossibleSignatures() const { return std::vector(signatures_.begin(), signatures_.end()); } -std::vector CharmDISFromSpline::GetPossibleTargets() const { +std::vector QuarkDISFromSpline::GetPossibleTargets() const { return std::vector(target_types_.begin(), target_types_.end()); } -std::vector CharmDISFromSpline::GetPossibleSignaturesFromParents(siren::dataclasses::ParticleType primary_type, siren::dataclasses::ParticleType target_type) const { +std::vector QuarkDISFromSpline::GetPossibleSignaturesFromParents(siren::dataclasses::ParticleType primary_type, siren::dataclasses::ParticleType target_type) const { std::pair key(primary_type, target_type); if(signatures_by_parent_types_.find(key) != signatures_by_parent_types_.end()) { return signatures_by_parent_types_.at(key); @@ -701,7 +926,7 @@ std::vector CharmDISFromSpline::GetPossibleSi } } -std::vector CharmDISFromSpline::DensityVariables() const { +std::vector QuarkDISFromSpline::DensityVariables() const { return std::vector{"Bjorken x", "Bjorken y"}; } diff --git a/projects/interactions/private/pybindings/QuarkDISFromSpline.h b/projects/interactions/private/pybindings/QuarkDISFromSpline.h new file mode 100644 index 000000000..65e60c44c --- /dev/null +++ b/projects/interactions/private/pybindings/QuarkDISFromSpline.h @@ -0,0 +1,90 @@ +#include +#include +#include + +#include +#include +#include + +#include "../../public/SIREN/interactions/CrossSection.h" +#include "../../public/SIREN/interactions/QuarkDISFromSpline.h" +#include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" +#include "../../../dataclasses/public/SIREN/dataclasses/InteractionRecord.h" +#include "../../../dataclasses/public/SIREN/dataclasses/InteractionSignature.h" +#include "../../../geometry/public/SIREN/geometry/Geometry.h" +#include "../../../utilities/public/SIREN/utilities/Random.h" + +void register_QuarkDISFromSpline(pybind11::module_ & m) { + using namespace pybind11; + using namespace siren::interactions; + + class_, CrossSection> quarkdisfromspline(m, "QuarkDISFromSpline"); + + quarkdisfromspline + + .def(init<>()) + .def(init, std::vector, int, double, double, std::set, std::set, std::string>(), + arg("total_xs_data"), + arg("differential_xs_data"), + arg("interaction"), + arg("target_mass"), + arg("minimum_Q2"), + arg("primary_types"), + arg("target_types"), + arg("units") = std::string("cm")) + .def(init, std::vector, int, double, double, std::vector, std::vector, std::string>(), + arg("total_xs_data"), + arg("differential_xs_data"), + arg("interaction"), + arg("target_mass"), + arg("minimum_Q2"), + arg("primary_types"), + arg("target_types"), + arg("units") = std::string("cm")) + .def(init, std::set, std::string>(), + arg("total_xs_filename"), + arg("differential_xs_filename"), + arg("interaction"), + arg("target_mass"), + arg("minimum_Q2"), + arg("primary_types"), + arg("target_types"), + arg("units") = std::string("cm")) + .def(init, std::set, std::string>(), + arg("total_xs_filename"), + arg("differential_xs_filename"), + arg("primary_types"), + arg("target_types"), + arg("units") = std::string("cm")) + .def(init, std::vector, std::string>(), + arg("total_xs_filename"), + arg("differential_xs_filename"), + arg("interaction"), + arg("target_mass"), + arg("minimum_Q2"), + arg("primary_types"), + arg("target_types"), + arg("units") = std::string("cm")) + .def(init, std::vector, std::string>(), + arg("total_xs_filename"), + arg("differential_xs_filename"), + arg("primary_types"), + arg("target_types"), + arg("units") = std::string("cm")) + .def(self == self) + .def("SetInteractionType",&QuarkDISFromSpline::SetInteractionType) + .def("SetQuarkType",&QuarkDISFromSpline::SetQuarkType) + .def("TotalCrossSection",overload_cast(&QuarkDISFromSpline::TotalCrossSection, const_)) + .def("TotalCrossSection",overload_cast(&QuarkDISFromSpline::TotalCrossSection, const_)) + .def("DifferentialCrossSection",overload_cast(&QuarkDISFromSpline::DifferentialCrossSection, const_)) + .def("DifferentialCrossSection",overload_cast(&QuarkDISFromSpline::DifferentialCrossSection, const_)) + .def("InteractionThreshold",&QuarkDISFromSpline::InteractionThreshold) + .def("FragmentationFraction",&QuarkDISFromSpline::FragmentationFraction) + .def("GetPossibleTargets",&QuarkDISFromSpline::GetPossibleTargets) + .def("GetPossibleTargetsFromPrimary",&QuarkDISFromSpline::GetPossibleTargetsFromPrimary) + .def("GetPossiblePrimaries",&QuarkDISFromSpline::GetPossiblePrimaries) + .def("GetPossibleSignatures",&QuarkDISFromSpline::GetPossibleSignatures) + .def("GetPossibleSignaturesFromParents",&QuarkDISFromSpline::GetPossibleSignaturesFromParents) + .def("FinalStateProbability",&QuarkDISFromSpline::FinalStateProbability); +} + diff --git a/projects/interactions/private/pybindings/interactions.cxx b/projects/interactions/private/pybindings/interactions.cxx index 7cfc91cda..9a2c628e2 100644 --- a/projects/interactions/private/pybindings/interactions.cxx +++ b/projects/interactions/private/pybindings/interactions.cxx @@ -6,6 +6,7 @@ #include "../../public/SIREN/interactions/InteractionCollection.h" #include "../../public/SIREN/interactions/DISFromSpline.h" #include "../../public/SIREN/interactions/CharmDISFromSpline.h" +#include "../../public/SIREN/interactions/QuarkDISFromSpline.h" #include "../../public/SIREN/interactions/HNLFromSpline.h" #include "../../public/SIREN/interactions/DipoleFromTable.h" #include "../../public/SIREN/interactions/DarkNewsCrossSection.h" @@ -21,6 +22,7 @@ #include "./DarkNewsDecay.h" #include "./DISFromSpline.h" #include "./CharmDISFromSpline.h" +#include "./QuarkDISFromSpline.h" #include "./HNLFromSpline.h" #include "./Decay.h" #include "./NeutrissimoDecay.h" @@ -55,6 +57,7 @@ PYBIND11_MODULE(interactions,m) { register_DarkNewsDecay(m); register_DISFromSpline(m); register_CharmDISFromSpline(m); + register_QuarkDISFromSpline(m); register_HNLFromSpline(m); register_NeutrissimoDecay(m); register_InteractionCollection(m); diff --git a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h index 62dc88a7d..f17139bfa 100644 --- a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h +++ b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h @@ -27,6 +27,8 @@ #include "SIREN/interactions/CrossSection.h" // for CrossSe... #include "SIREN/dataclasses/InteractionSignature.h" // for Interac... #include "SIREN/dataclasses/Particle.h" // for Particle +#include "SIREN/utilities/Interpolator.h" +#include "SIREN/utilities/Integration.h" namespace siren { namespace dataclasses { class InteractionRecord; } } namespace siren { namespace utilities { class SIREN_random; } } @@ -45,10 +47,18 @@ friend cereal::access; std::set target_types_; std::map> targets_by_primary_types_; std::map, std::vector> signatures_by_parent_types_; - + std::set D_types_; + + // used by the DIS process int interaction_type_; + int quark_type_; double target_mass_; double minimum_Q2_; + + // used by the hadronization process + double fragmentation_integral = 0; // for storing the integrated unnormed pdf + void normalize_pdf(); // for normalizing pdf and integral, to be called at initialization + siren::utilities::Interpolator1D inverseCdfTable; // for storing the CDF-1 table for the hadronization double unit; @@ -62,36 +72,44 @@ friend cereal::access; QuarkDISFromSpline(std::string differential_filename, std::string total_filename, std::vector primary_types, std::vector target_types, std::string units = "cm"); void SetUnits(std::string units); - // this might be integrated later? could also make another initialization method - // problem with current implementation is that EM is not supported b/c at initialization we assume int = 1 - // this sets the isoscalar target mass void SetInteractionType(int interaction); + void SetQuarkType(int q_type); virtual bool equal(CrossSection const & other) const override; + // function definitions needed to compute the DIS vertex double TotalCrossSection(dataclasses::InteractionRecord const &) const override; double TotalCrossSection(siren::dataclasses::ParticleType primary, double energy) const; double DifferentialCrossSection(dataclasses::InteractionRecord const &) const override; double DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2=std::numeric_limits::quiet_NaN()) const; double InteractionThreshold(dataclasses::InteractionRecord const &) const override; - void SampleFinalState(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr random) const override; + // function definitions needed to compute the hadronization vertex + double FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const; + double sample_pdf(double z) const; + void compute_cdf(); + static double getHadronMass(siren::dataclasses::ParticleType hadron_type); + + // used for both processes + void SampleFinalState(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr random) const override; std::vector GetPossibleTargets() const override; std::vector GetPossibleTargetsFromPrimary(siren::dataclasses::ParticleType primary_type) const override; std::vector GetPossiblePrimaries() const override; std::vector GetPossibleSignatures() const override; std::vector GetPossibleSignaturesFromParents(siren::dataclasses::ParticleType primary_type, siren::dataclasses::ParticleType target_type) const override; - virtual double FinalStateProbability(dataclasses::InteractionRecord const & record) const override; + // other utility functions void LoadFromFile(std::string differential_filename, std::string total_filename); void LoadFromMemory(std::vector & differential_data, std::vector & total_data); + // utilities for DIS parametrs double GetMinimumQ2() const {return minimum_Q2_;}; double GetTargetMass() const {return target_mass_;}; int GetInteractionType() const {return interaction_type_;}; - static double GetLeptonMass(siren::dataclasses::ParticleType lepton_type); + static std::map getIndices(siren::dataclasses::InteractionSignature signature); + public: virtual std::vector DensityVariables() const override; diff --git a/python/SIREN_Controller.py b/python/SIREN_Controller.py index 7daf7721a..dc725e2e5 100644 --- a/python/SIREN_Controller.py +++ b/python/SIREN_Controller.py @@ -489,6 +489,7 @@ def GenerateEvents(self, N=None, fill_tables_at_exit=True): self.global_times.append(t-self.global_start) prev_time = t count += 1 + # print("finished generating one events") if hasattr(self, "DN_processes"): self.DN_processes.SaveCrossSectionTables(fill_tables_at_exit=fill_tables_at_exit) return self.events From 4dfb9ce392e72ad96c7bea0e8f1c9c7526292dfd Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Wed, 11 Dec 2024 23:07:42 -0500 Subject: [PATCH 11/93] quarkDIS completed --- .../interactions/private/CharmMesonDecay.cxx | 7 +- .../private/QuarkDISFromSpline.cxx | 67 +++++++++++++------ 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx index 5b92f0f1a..8f1832193 100644 --- a/projects/interactions/private/CharmMesonDecay.cxx +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -367,8 +367,11 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco // now finally perform the last aximuthal rotation double W_phi = random->Uniform(0, 2 * M_PI); geom3::Rotation3 W_azimuth_rand_rot(p3W_lab_dir, W_phi); - rk::P4 p4l_lab = p4l_Wrest.rotate(W_azimuth_rand_rot); - rk::P4 p4nu_lab = p4nu_Wrest.rotate(W_azimuth_rand_rot); + p4l_Wrest.rotate(W_azimuth_rand_rot); + p4nu_Wrest.rotate(W_azimuth_rand_rot); + rk::Boost boost_from_Wrest_to_lab = p4W_lab.labBoost(); + rk::P4 p4l_lab = p4l_Wrest.boost(boost_from_Wrest_to_lab); + rk::P4 p4nu_lab = p4nu_Wrest.boost(boost_from_Wrest_to_lab); std::vector & secondaries = record.GetSecondaryParticleRecords(); siren::dataclasses::SecondaryParticleRecord & kpi = secondaries[0]; diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 0c1842e0c..608c07def 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -62,7 +62,7 @@ bool kinematicallyAllowed(double x, double y, double E, double M, double m) { double s = 2 * M * E; double Q2 = s * x * y; double Mc = siren::utilities::Constants::D0Mass; - return ((ad - bd) <= d * y and d * y <= (ad + bd)) && (Q2 / (1 - x) + pow(M, 2) >= pow(M + Mc, 2)); //Eq. 7 + return ((ad - bd) <= d * y and d * y <= (ad + bd)) && (Q2 * (1 - x) / x + pow(M, 2) >= pow(M + Mc, 2)); //Eq. 7 } } @@ -577,7 +577,7 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR } rk::P4 p1(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); // std::cout << "quark::sampleFinalState : primary momentum is read to be " << p1 << std::endl; - rk::P4 p2(geom3::Vector3(0, 0, 0), record.target_mass); + rk::P4 p2(geom3::Vector3(0, 0, 0), target_mass_); // we assume that: // the target is stationary so its energy is just its mass @@ -813,6 +813,7 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR int sampling = 0; // sample again if this eenrgy is not kinematically allowed + // this samples in the lab frame the energy of the D-meson such that mass is real do { sampling += 1; if (sampling > max_sampling) { @@ -829,27 +830,55 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR } else { frag_accept = true; } - double test_ED = ECH; - double test_EX = (1-z) * Ec; - double test_p3D = std::sqrt(std::pow(test_ED, 2) - std::pow(mCH, 2)); - double test_rD = test_p3D / p3c; - double test_p3X = std::pow((1 - test_rD), 2) * std::pow(p3c, 2); - if (std::pow(test_EX, 2) - std::pow(test_p3X, 2) <= 0) {frag_accept = false;} else {frag_accept = true;} } while (!frag_accept); + // new attempt of using the isoscalar mass as the remnant hadronic shower mass + double mX = target_mass_; + double Mc = p4.m(); + // std::cout << "using remnant mass " << mX << std::endl; + // std::cout << "invariant charm mass and its energy is " << Mc << ", " << p4.e() << std::endl; + // std::cout << "target sampled D meson energy is " << ECH << std::endl; + // std::cout << "and the fraction of momentum is sampled to be " << z << std::endl; + //compute the energies in the charm rest frame + double E_CH_c = (std::pow(Mc, 2) - std::pow(mX, 2) + std::pow(mCH, 2)) / (2 * Mc); + // std::cout << "energy of charm in rest frame is " << E_CH_c << std::endl; + double p_c = std::sqrt((std::pow(Mc, 2) - std::pow(mCH + mX, 2)) * (std::pow(Mc, 2) - std::pow(mCH - mX, 2))) / (2 * Mc); + // std::cout << "momentum in charm rest frame is " << p_c << std::endl; + // compute the lorentz boost parameters + double gamma = p4.gamma(); + double beta = p4.beta(); + // std::cout << "beta and gamma parameters are " << beta << ", " << gamma << std::endl; + // using the lab frame fragmented energy and the + double cosTheta = std::max(std::min(((ECH - gamma * E_CH_c)/(gamma * beta * p_c)), 1.), -1.); + // std::cout << "cosine of theta in charm frame is " << cosTheta << std::endl; + // std::cout << "without cutting, the number is " << (ECH - gamma * E_CH_c)/(gamma * beta * p_c) << std::endl; + // now compute the momentum vectors in the rest frame + double sinTheta = std::sin(std::acos(cosTheta)); + // std::cout << "and sine of theta is computed to be " << sinTheta << std::endl; + rk::P4 p4CH_c(p_c * geom3::Vector3(cosTheta, sinTheta, 0), mCH); + rk::P4 p4X_c(p_c * geom3::Vector3(-cosTheta, -sinTheta, 0), mX); + // these all assume boost direction is charm direction. Now we should rotate back to charm lab momentum direction + geom3::Vector3 pc_lab_momentum = p4.momentum(); + geom3::UnitVector3 pc_lab_dir = pc_lab_momentum.direction(); + geom3::Rotation3 x_to_pc_lab_rot = geom3::rotationBetween(x_dir, pc_lab_dir); + p4X_c.rotate(x_to_pc_lab_rot); + p4CH_c.rotate(x_to_pc_lab_rot); + + // finally, we perform a random azimuthal rotation + double c_phi = random->Uniform(0, 2 * M_PI); + geom3::Rotation3 azimuth_rand_rot(pc_lab_dir, c_phi); + p4X_c.rotate(azimuth_rand_rot); + p4CH_c.rotate(azimuth_rand_rot); - // set the 3-momentum of the charmed meson and subsequently the 4-momentum - double p3CH = std::sqrt(std::pow(ECH, 2) - std::pow(mCH, 2)); //obtain charmed hadron 3-momentum - double rCH = p3CH/p3c; // ratio of momentum carried away by the charmed hadron, assume collinearity - rk::P4 p4CH(geom3::Vector3(rCH * record.primary_momentum[1], rCH * record.primary_momentum[2], rCH * record.primary_momentum[3]), mCH); + // and boost them back to the lab frame + rk::Boost boost_from_crest_to_lab = p4.labBoost(); + rk::P4 p4X = p4X_c.boost(boost_from_crest_to_lab); + rk::P4 p4CH = p4CH_c.boost(boost_from_crest_to_lab); - // the 4 momentum (and the mass) of the resulting hadronic vertex is determined solely via 4-momentum conservation - rk::P4 p4X = p4 - p4CH; - // let's first assume massless hadron final state - // double EX = (1 - z) * Ec; // energy of the hadronic shower - // double p3X = EX; // assume no hadronic mass - // double rX = p3X/p3c; // assume collinear - // rk::P4 p4X(geom3::Vector3(rX * record.primary_momentum[1], rX * record.primary_momentum[2], rX * record.primary_momentum[3]), 0); + // std::cout << "computed remnant mass and energy is " << p4X.m() << ", " << p4X.e() << std::endl; + // std::cout << "and computed D mass and energy is " << p4CH.m() << ", " << p4CH.e() << std::endl; + // std::cout << "target sampled D meson energy is " << ECH << std::endl; + // now we proceed to saving the final state kinematics std::vector & secondaries = record.GetSecondaryParticleRecords(); siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[lepton_index]; From 5a40bffb34d9d5fb768f1c202fdc29ca67174145 Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Sun, 2 Feb 2025 21:42:16 -0500 Subject: [PATCH 12/93] fix current type issue --- projects/injection/private/Injector.cxx | 4 +++- projects/interactions/private/DMesonELoss.cxx | 12 ++++++++++-- .../interactions/private/QuarkDISFromSpline.cxx | 16 ++++++++++++---- .../private/pybindings/QuarkDISFromSpline.h | 12 ++++++++---- .../SIREN/interactions/QuarkDISFromSpline.h | 8 ++++---- 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/projects/injection/private/Injector.cxx b/projects/injection/private/Injector.cxx index 197da2c84..d3bcd25c7 100644 --- a/projects/injection/private/Injector.cxx +++ b/projects/injection/private/Injector.cxx @@ -375,11 +375,13 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { std::shared_ptr parent = tree.add_entry(record); // Secondary Processes - //std::cout << "injector::GenerateEvent : sampling secondary process" << std::endl; + // std::cout << "injector::GenerateEvent : sampling secondary process" << std::endl; std::deque, std::shared_ptr>> secondaries; std::function)> add_secondaries = [&](std::shared_ptr parent) { for(size_t i=0; irecord.signature.secondary_types.size(); ++i) { siren::dataclasses::ParticleType const & type = parent->record.signature.secondary_types[i]; + // std::cout << "parent type" << parent->record.signature.secondary_types[i] << std::endl; + std::map>::iterator it = secondary_process_map.find(type); if(it == secondary_process_map.end()) { continue; diff --git a/projects/interactions/private/DMesonELoss.cxx b/projects/interactions/private/DMesonELoss.cxx index 6454c3a63..2d61e2d63 100644 --- a/projects/interactions/private/DMesonELoss.cxx +++ b/projects/interactions/private/DMesonELoss.cxx @@ -77,8 +77,9 @@ std::vector DMesonELoss::GetPossibleSignature signature.target_type = target_type; // first we deal with semileptonic decays where there are 3 final state particles - signature.secondary_types.resize(1); + signature.secondary_types.resize(2); signature.secondary_types[0] = primary_type; // same particle comes out + signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::Hadrons; // there also is a hadronic vertex signatures.push_back(signature); return signatures; } @@ -204,15 +205,22 @@ void DMesonELoss::SampleFinalState(dataclasses::CrossSectionDistributionRecord& double p3i = std::sqrt(std::pow(p1.px(), 2) + std::pow(p1.py(), 2) + std::pow(p1.pz(), 2)); double p_ratio = p3f / p3i; rk::P4 pf(p_ratio * geom3::Vector3(p1.px(), p1.py(), p1.pz()), Dmass); + double E_H = primary_energy - pf.e(); // rest of the energy go into the nucleus + double p_H = E_H; // assume collinearity, energy conservation and not momentum conservation ie massless (for now) + double H_ratio = p_H / p3i; + rk::P4 p4_H(H_ratio * geom3::Vector3(p1.px(), p1.py(), p1.pz()), 0); std::vector & secondaries = interaction.GetSecondaryParticleRecords(); siren::dataclasses::SecondaryParticleRecord & dmeson = secondaries[0]; - + siren::dataclasses::SecondaryParticleRecord & hadron = secondaries[1]; dmeson.SetFourMomentum({pf.e(), pf.px(), pf.py(), pf.pz()}); dmeson.SetMass(pf.m()); dmeson.SetHelicity(interaction.primary_helicity); + hadron.SetFourMomentum({p4_H.e(), p4_H.px(), p4_H.py(), p4_H.pz()}); + hadron.SetMass(p4_H.m()); + hadron.SetHelicity(interaction.primary_helicity); } double DMesonELoss::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 608c07def..72064f41e 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -72,26 +72,32 @@ QuarkDISFromSpline::QuarkDISFromSpline() { compute_cdf(); } -QuarkDISFromSpline::QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { +QuarkDISFromSpline::QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, int quark_type, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), quark_type_(quark_type), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { normalize_pdf(); compute_cdf(); LoadFromMemory(differential_data, total_data); + SetInteractionType(interaction); + SetQuarkType(quark_type); InitializeSignatures(); SetUnits(units); } -QuarkDISFromSpline::QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { +QuarkDISFromSpline::QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, int quark_type, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction),quark_type_(quark_type), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { normalize_pdf(); compute_cdf(); LoadFromMemory(differential_data, total_data); + SetInteractionType(interaction); + SetQuarkType(quark_type); InitializeSignatures(); SetUnits(units); } -QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { +QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, int quark_type, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), quark_type_(quark_type), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { normalize_pdf(); compute_cdf(); LoadFromFile(differential_filename, total_filename); + SetInteractionType(interaction); + SetQuarkType(quark_type); InitializeSignatures(); SetUnits(units); } @@ -105,10 +111,12 @@ QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::s SetUnits(units); } -QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { +QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, int quark_type, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), quark_type_(quark_type), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { normalize_pdf(); compute_cdf(); LoadFromFile(differential_filename, total_filename); + SetInteractionType(interaction); + SetQuarkType(quark_type); InitializeSignatures(); SetUnits(units); } diff --git a/projects/interactions/private/pybindings/QuarkDISFromSpline.h b/projects/interactions/private/pybindings/QuarkDISFromSpline.h index 65e60c44c..46e68053d 100644 --- a/projects/interactions/private/pybindings/QuarkDISFromSpline.h +++ b/projects/interactions/private/pybindings/QuarkDISFromSpline.h @@ -23,28 +23,31 @@ void register_QuarkDISFromSpline(pybind11::module_ & m) { quarkdisfromspline .def(init<>()) - .def(init, std::vector, int, double, double, std::set, std::set, std::string>(), + .def(init, std::vector, int, int, double, double, std::set, std::set, std::string>(), arg("total_xs_data"), arg("differential_xs_data"), arg("interaction"), + arg("quark_type"), arg("target_mass"), arg("minimum_Q2"), arg("primary_types"), arg("target_types"), arg("units") = std::string("cm")) - .def(init, std::vector, int, double, double, std::vector, std::vector, std::string>(), + .def(init, std::vector, int, int, double, double, std::vector, std::vector, std::string>(), arg("total_xs_data"), arg("differential_xs_data"), arg("interaction"), + arg("quark_type"), arg("target_mass"), arg("minimum_Q2"), arg("primary_types"), arg("target_types"), arg("units") = std::string("cm")) - .def(init, std::set, std::string>(), + .def(init, std::set, std::string>(), arg("total_xs_filename"), arg("differential_xs_filename"), arg("interaction"), + arg("quark_type"), arg("target_mass"), arg("minimum_Q2"), arg("primary_types"), @@ -56,10 +59,11 @@ void register_QuarkDISFromSpline(pybind11::module_ & m) { arg("primary_types"), arg("target_types"), arg("units") = std::string("cm")) - .def(init, std::vector, std::string>(), + .def(init, std::vector, std::string>(), arg("total_xs_filename"), arg("differential_xs_filename"), arg("interaction"), + arg("quark_type"), arg("target_mass"), arg("minimum_Q2"), arg("primary_types"), diff --git a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h index f17139bfa..8a30eab86 100644 --- a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h +++ b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h @@ -64,11 +64,11 @@ friend cereal::access; public: QuarkDISFromSpline(); - QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minumum_Q2, std::set primary_types, std::set target_types, std::string units = "cm"); - QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minumum_Q2, std::vector primary_types, std::vector target_types, std::string units = "cm"); - QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minumum_Q2, std::set primary_types, std::set target_types, std::string units = "cm"); + QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, int quark_type, double target_mass, double minumum_Q2, std::set primary_types, std::set target_types, std::string units = "cm"); + QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, int quark_type, double target_mass, double minumum_Q2, std::vector primary_types, std::vector target_types, std::string units = "cm"); + QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, int quark_type, double target_mass, double minumum_Q2, std::set primary_types, std::set target_types, std::string units = "cm"); QuarkDISFromSpline(std::string differential_filename, std::string total_filename, std::set primary_types, std::set target_types, std::string units = "cm"); - QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minumum_Q2, std::vector primary_types, std::vector target_types, std::string units = "cm"); + QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, int quark_type, double target_mass, double minumum_Q2, std::vector primary_types, std::vector target_types, std::string units = "cm"); QuarkDISFromSpline(std::string differential_filename, std::string total_filename, std::vector primary_types, std::vector target_types, std::string units = "cm"); void SetUnits(std::string units); From 526c057d2546346ac15983c015cdead8fb654fd1 Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Mon, 31 Mar 2025 16:32:05 -0400 Subject: [PATCH 13/93] newest update for Di Muon extensions preparations (Pavel) --- .../interactions/private/CharmMesonDecay.cxx | 103 +++++++++++++----- .../private/QuarkDISFromSpline.cxx | 3 +- .../SIREN/interactions/CharmMesonDecay.h | 1 + 3 files changed, 79 insertions(+), 28 deletions(-) diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx index 8f1832193..f70752b16 100644 --- a/projects/interactions/private/CharmMesonDecay.cxx +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -147,12 +147,23 @@ double CharmMesonDecay::TotalDecayWidthForFinalState(dataclasses::InteractionRec std::set kminus_eplus_nue = {siren::dataclasses::Particle::ParticleType::KMinus, siren::dataclasses::Particle::ParticleType::EPlus, siren::dataclasses::Particle::ParticleType::NuE}; - if (primary == siren::dataclasses::Particle::ParticleType::DPlus && secondaries == k0_eplus_nue) { - branching_ratio = 1; - tau = 1040 * (1e-15); - } else if (primary == siren::dataclasses::Particle::ParticleType::D0 && secondaries == kminus_eplus_nue) { - branching_ratio = 1; - tau = 410.1 * (1e-15); + std::set k0_muplus_numu = {siren::dataclasses::Particle::ParticleType::K0Bar, + siren::dataclasses::Particle::ParticleType::MuPlus, + siren::dataclasses::Particle::ParticleType::NuMu}; + std::set kminus_muplus_numu = {siren::dataclasses::Particle::ParticleType::KMinus, + siren::dataclasses::Particle::ParticleType::MuPlus, + siren::dataclasses::Particle::ParticleType::NuMu}; + std::set hadrons = {siren::dataclasses::Particle::ParticleType::Hadrons}; + if (primary == siren::dataclasses::Particle::ParticleType::DPlus) { + tau = 1040 * (1e-15); + if (secondaries == k0_eplus_nue) {branching_ratio = .1607;} // e+ semileptonic mode according to pdg + else if (secondaries == k0_muplus_numu) {branching_ratio = .176;} // mu+ anything according to pdg + else if (secondaries == hadrons) {branching_ratio = (1 - .1607 - .176);} // everything else + } else if (primary == siren::dataclasses::Particle::ParticleType::D0) { + tau = 410.1 * (1e-15); + if (secondaries == kminus_eplus_nue) {branching_ratio = .0649;} // e+ semileptonic mode according to pdg + else if (secondaries == kminus_muplus_numu) {branching_ratio = .067;} // mu+ anything according to pdg + else if (secondaries == hadrons) {branching_ratio = (1 - .0649 - .067);} // everything else } else { std::cout << "this decay mode is not yet implemented!" << std::endl; @@ -172,22 +183,41 @@ std::vector CharmMesonDecay::GetPossibleSigna std::vector CharmMesonDecay::GetPossibleSignaturesFromParent(siren::dataclasses::Particle::ParticleType primary) const { std::vector signatures; - dataclasses::InteractionSignature signature; - signature.primary_type = primary; - signature.target_type = siren::dataclasses::Particle::ParticleType::Decay; - - // first we deal with semileptonic decays where there are 3 final state particles - signature.secondary_types.resize(3); - if(primary==siren::dataclasses::Particle::ParticleType::DPlus) { - signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::K0Bar; - signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::EPlus; - signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuE; - signatures.push_back(signature); + // initialize semileptonic signatures + dataclasses::InteractionSignature semilep_signature; + semilep_signature.primary_type = primary; + semilep_signature.target_type = siren::dataclasses::Particle::ParticleType::Decay; + semilep_signature.secondary_types.resize(3); + // initialize other signatures + dataclasses::InteractionSignature hadron_signature; + hadron_signature.primary_type = primary; + hadron_signature.target_type = siren::dataclasses::Particle::ParticleType::Decay; + hadron_signature.secondary_types.resize(1); + + if (primary==siren::dataclasses::Particle::ParticleType::DPlus) { + // semi-leptonic modes with muon and electron + semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::K0Bar; + semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::EPlus; + semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuE; + signatures.push_back(semilep_signature); + semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::K0Bar; + semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::MuPlus; + semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuMu; + signatures.push_back(semilep_signature); + // all other modes implemented as one big bang + hadron_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; + signatures.push_back(hadron_signature); } else if (primary==siren::dataclasses::Particle::ParticleType::D0) { - signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::KMinus; - signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::EPlus; - signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuE; - signatures.push_back(signature); + semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::KMinus; + semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::EPlus; + semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuE; + signatures.push_back(semilep_signature); + semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::KMinus; + semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::MuPlus; + semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuMu; + signatures.push_back(semilep_signature); + hadron_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; + signatures.push_back(hadron_signature); } else { std::cout << "this D meson decay has not been implemented yet" << std::endl; @@ -213,6 +243,11 @@ std::vector CharmMesonDecay::FormFactorFromRecord(dataclasses::CrossSect } double CharmMesonDecay::DifferentialDecayWidth(dataclasses::InteractionRecord const & record) const { + // first let the fully hadronic state be handled separately + dataclasses::InteractionSignature signature = record.signature; + if (signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::Hadrons) { + return TotalDecayWidthForFinalState(record); + } // get the form factor constants std::vector constants = FormFactorFromRecord(record); // calculate the q^2 @@ -300,14 +335,27 @@ void CharmMesonDecay::computeDiffGammaCDF(std::vector constants, double inverse_cdf_data.f = cdf_Q2_nodes; inverseCdf = siren::utilities::Interpolator1D(inverse_cdf_data); - return; } - +// this is temporary implementation +void CharmMesonDecay::SampleFinalStateHadronic(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { + std::vector & secondaries = record.GetSecondaryParticleRecords(); + siren::dataclasses::SecondaryParticleRecord & hadrons = secondaries[0]; + rk::P4 p4D_lab(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); + hadrons.SetFourMomentum({p4D_lab.e(), p4D_lab.px(), p4D_lab.py(), p4D_lab.pz()}); + hadrons.SetMass(p4D_lab.m()); + hadrons.SetHelicity(record.primary_helicity); +} void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { + // first handle hadronic decay separately, in the end we want to handle all decays in separate functions + dataclasses::InteractionSignature signature = record.signature; + if (signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::Hadrons) { + SampleFinalStateHadronic(record, random); + return; + } // first obtain the constants needed for further computation from the signature std::vector constants = FormFactorFromRecord(record); double mD = particleMass(record.signature.primary_type); @@ -328,14 +376,14 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco geom3::UnitVector3 p3D_lab_dir = p3D_lab.direction(); geom3::Rotation3 x_to_p3D_lab_rot = geom3::rotationBetween(x_dir, p3D_lab_dir); // compute the momentum magnitude of the W and the K/pi - double EK = 0.5 * (Q2 - pow(mD, 2) + pow(mK, 2)) / mD; // energy of Kaon - double PK = pow(pow(EK, 2) - pow(mK, 2), 1/2); // momentum magnitude of kaon in D rest frame + double EK = 0.5 * (pow(mD, 2) + pow(mK, 2) - Q2) / mD; // energy of Kaon + double PK = pow(pow(EK, 2) - pow(mK, 2), .5); // momentum magnitude of kaon in D rest frame double PW = sqrt(Q2); // momentum magnitude of virtual W in D rest frame // compute the 3 vectors of the W and the K/pi in the D rest frame, defined wrt x axis rk::P4 p4K_Drest(PK * geom3::Vector3(cosTheta, sinTheta, 0), mK); - rk::P4 p4W_Drest(PW * geom3::Vector3(-cosTheta, -sinTheta, 0), PW); // invariant mass assigned to virtual W boson - // rotate the momentum vectors so they are defined wrt to the D lab frame direction + rk::P4 p4W_Drest(PK * geom3::Vector3(-cosTheta, -sinTheta, 0), PW); // invariant mass assigned to virtual W boson + // rotate the momentum vectors so they are defined wrt to the D lab frame direction p4K_Drest.rotate(x_to_p3D_lab_rot); p4W_Drest.rotate(x_to_p3D_lab_rot); // perform the random "azimuth" rotation @@ -347,6 +395,7 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco rk::Boost boost_from_Drest_to_lab = p4D_lab.labBoost(); rk::P4 p4K_lab = p4K_Drest.boost(boost_from_Drest_to_lab); rk::P4 p4W_lab = p4W_Drest.boost(boost_from_Drest_to_lab); + // this ends the computation of D->W+K/Pi decay, now treat the W->l+nu decay double ml = particleMass(record.signature.secondary_types[1]); double mnu = 0; diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 72064f41e..fa5b091fb 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -930,10 +930,11 @@ double QuarkDISFromSpline::FinalStateProbability(dataclasses::InteractionRecord unsigned int meson_index = secondaries["meson"]; double fragfrac = FragmentationFraction(interaction.signature.secondary_types[meson_index]); if(dxs == 0) { + std::cout << "diff xsec gives 0" << std::endl; return 0.0; } else { // if (txs == 0) {std::cout << "wtf??? txs is 0 in final state prob" << txs << std::endl;} - // if (std::isinf(dxs)) {std::cout << "dxs is inf in final state prob" << std::endl;} + if (std::isinf(dxs)) {std::cout << "dxs is inf in final state prob" << std::endl;} return dxs / txs * fragfrac; } } diff --git a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h index bcb66b6a2..c5bf3e84f 100644 --- a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h +++ b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h @@ -46,6 +46,7 @@ friend cereal::access; double TotalDecayWidthForFinalState(dataclasses::InteractionRecord const &) const override; double DifferentialDecayWidth(dataclasses::InteractionRecord const &) const override; double DifferentialDecayWidth(std::vector constants, double Q2, double mD, double mK) const; + void SampleFinalStateHadronic(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr) const; void SampleFinalState(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr) const override; std::vector GetPossibleSignatures() const override; std::vector GetPossibleSignaturesFromParent(siren::dataclasses::Particle::ParticleType primary) const override; From 10d86d59f7cbb7a09c685c1d26b71bb46bf4c4a1 Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Mon, 8 Dec 2025 14:24:06 -0500 Subject: [PATCH 14/93] update before codebasing into WHAMS --- .../Detectors/densities/IceCube/IceCube-v1.dat | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/resources/Detectors/densities/IceCube/IceCube-v1.dat b/resources/Detectors/densities/IceCube/IceCube-v1.dat index 6f8fa1f69..32abf6804 100644 --- a/resources/Detectors/densities/IceCube/IceCube-v1.dat +++ b/resources/Detectors/densities/IceCube/IceCube-v1.dat @@ -8,6 +8,7 @@ # Uses PREM model of the Earth # Assumes ice is a spherical cap on the earth # Assumes IceCube is in clear ice +# DeepCore addition by N. Kamp object sphere 0 0 0 0 0 0 6478000 atmo_radius AIR radial_polynomial 0 0 0 1 0.000811 # 0.673atm x 1.205e-3(g/cm3 for 1atm) object sphere 0 0 0 0 0 0 6374134 iceair_boundary ICE radial_polynomial 0 0 0 1 0.762944 # surface of ice, 0.832 x 0.917 @@ -24,9 +25,19 @@ object sphere 0 0 0 0 0 0 1221500 innercore_boundary INNERCORE r # IceCube detector: 1km^3 cylinder of ice object cylinder 0 0 6372184 0 0 0 564.19 0 1000 icecube ICE radial_polynomial 0 0 0 1 0.921585 # same as rockice +object cylinder 0 0 6372184 0 0 0 614.19 0 1100 icecube_shell ICE radial_polynomial 0 0 0 1 0.921585 # same as rockice + +# DeepCore detector: smaller cylinder in bottom half of the full array +# rough approximation from https://arxiv.org/pdf/1109.6096 and https://wiki.icecube.wisc.edu/index.php/Surface_coordinates +object cylinder 50 -50 6371859 0 0 0 150 0 350 deepcore ICE radial_polynomial 0 0 0 1 0.921585 # same as rockice +object cylinder 50 -50 6371859 0 0 0 200 0 450 deepcore_shell ICE radial_polynomial 0 0 0 1 0.921585 # same as rockice + # center of detector at IceCube detector 0 0 6372184 # Fiducial volume -fiducial cylinder 0 0 0 0 0 0 564.19 0 1000 \ No newline at end of file +# This one is the 1km^3 detector +# fiducial cylinder 0 0 0 0 0 0 564.19 0 1000 +# This one is a shell enlarged by 50m (effective scattering length) +fiducial cylinder 0 0 0 0 0 0 664.19 0 1200 \ No newline at end of file From 4a70aecb57f63e811bc68f99ab63f99ab5d9e883 Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Wed, 25 Mar 2026 00:44:40 -0400 Subject: [PATCH 15/93] fixed TotalCrossSection for QuarkDISFromSpline to take signature and avoid double counting --- .gitignore | 3 + .../private/QuarkDISFromSpline.cxx | 18 +- .../private/QuarkDISFromSpline.cxx.bak | 972 ++++++++++++++++++ 3 files changed, 987 insertions(+), 6 deletions(-) create mode 100644 projects/interactions/private/QuarkDISFromSpline.cxx.bak diff --git a/.gitignore b/.gitignore index c57fe85da..03354431a 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,6 @@ __pycache__/ # output dirs output/ + +# vendors +vendor/ \ No newline at end of file diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index fa5b091fb..288c1c06f 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -450,7 +450,16 @@ double QuarkDISFromSpline::TotalCrossSection(dataclasses::InteractionRecord cons std::cout << "DIS::interaction threshold not satisfied" << std::endl; return 0; } - return TotalCrossSection(primary_type, primary_energy); + double total_xs = TotalCrossSection(primary_type, primary_energy); + // Apply fragmentation fraction for the specific D meson in this signature + // so that summing over signatures (D0 + D+) gives the correct total + for (auto const & sec_type : interaction.signature.secondary_types) { + if (siren::dataclasses::isD(sec_type)) { + total_xs *= FragmentationFraction(sec_type); + break; + } + } + return total_xs; } double QuarkDISFromSpline::TotalCrossSection(siren::dataclasses::ParticleType primary_type, double primary_energy) const { @@ -925,17 +934,14 @@ double QuarkDISFromSpline::FinalStateProbability(dataclasses::InteractionRecord // std::cout << "diff xsec gives 0" << std::endl; // } double txs = TotalCrossSection(interaction); - //then compute the fragmentation probability - std::map secondaries = getIndices(interaction.signature); - unsigned int meson_index = secondaries["meson"]; - double fragfrac = FragmentationFraction(interaction.signature.secondary_types[meson_index]); + // fragmentation fraction is now applied inside TotalCrossSection if(dxs == 0) { std::cout << "diff xsec gives 0" << std::endl; return 0.0; } else { // if (txs == 0) {std::cout << "wtf??? txs is 0 in final state prob" << txs << std::endl;} if (std::isinf(dxs)) {std::cout << "dxs is inf in final state prob" << std::endl;} - return dxs / txs * fragfrac; + return dxs / txs; } } diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx.bak b/projects/interactions/private/QuarkDISFromSpline.cxx.bak new file mode 100644 index 000000000..fa5b091fb --- /dev/null +++ b/projects/interactions/private/QuarkDISFromSpline.cxx.bak @@ -0,0 +1,972 @@ +#include "SIREN/interactions/QuarkDISFromSpline.h" + +#include // for map, opera... +#include // for set, opera... +#include // for array +#include // for pow, log10 +#include // for tie, opera... +#include // for allocator +#include // for basic_string +#include // for vector +#include // for assert +#include // for size_t +#include +#include +#include +#include +#include + +#include // for P4, Boost +#include // for Vector3 + +#include // for splinetable +//#include + +#include "SIREN/interactions/CrossSection.h" // for CrossSection +#include "SIREN/dataclasses/InteractionRecord.h" // for Interactio... +#include "SIREN/dataclasses/Particle.h" // for Particle +#include "SIREN/utilities/Random.h" // for SIREN_random +#include "SIREN/utilities/Constants.h" // for electronMass +#include "SIREN/utilities/Errors.h" // for PythonImplementationError + + +namespace siren { +namespace interactions { + +namespace { +///Check whether a given point in phase space is physically realizable. +///Based on equations 6-8 of http://dx.doi.org/10.1103/PhysRevD.66.113007 +///S. Kretzer and M. H. Reno +///"Tau neutrino deep inelastic charged current interactions" +///Phys. Rev. D 66, 113007 +///\param x Bjorken x of the interaction +///\param y Bjorken y of the interaction +///\param E Incoming neutrino in energy in the lab frame ($E_\nu$) +///\param M Mass of the target nucleon ($M_N$) +///\param m Mass of the secondary lepton ($m_\tau$) +bool kinematicallyAllowed(double x, double y, double E, double M, double m) { + if(x > 1) //Eq. 6 right inequality + return false; + if(x < ((m * m) / (2 * M * (E - m)))) //Eq. 6 left inequality + return false; + if (x < 1e-6 || y < 1e-6) return false; + + //denominator of a and b + double d = 2 * (1 + (M * x) / (2 * E)); + //the numerator of a (or a*d) + double ad = 1 - m * m * ((1 / (2 * M * E * x)) + (1 / (2 * E * E))); + double term = 1 - ((m * m) / (2 * M * E * x)); + //the numerator of b (or b*d) + double bd = sqrt(term * term - ((m * m) / (E * E))); + + double s = 2 * M * E; + double Q2 = s * x * y; + double Mc = siren::utilities::Constants::D0Mass; + return ((ad - bd) <= d * y and d * y <= (ad + bd)) && (Q2 * (1 - x) / x + pow(M, 2) >= pow(M + Mc, 2)); //Eq. 7 +} +} + +QuarkDISFromSpline::QuarkDISFromSpline() { + // initialize the pdf normalization and cdf table for the hadronization process + normalize_pdf(); + compute_cdf(); +} + +QuarkDISFromSpline::QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, int quark_type, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), quark_type_(quark_type), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { + normalize_pdf(); + compute_cdf(); + LoadFromMemory(differential_data, total_data); + SetInteractionType(interaction); + SetQuarkType(quark_type); + InitializeSignatures(); + SetUnits(units); +} + +QuarkDISFromSpline::QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, int quark_type, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction),quark_type_(quark_type), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { + normalize_pdf(); + compute_cdf(); + LoadFromMemory(differential_data, total_data); + SetInteractionType(interaction); + SetQuarkType(quark_type); + InitializeSignatures(); + SetUnits(units); +} + +QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, int quark_type, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), quark_type_(quark_type), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { + normalize_pdf(); + compute_cdf(); + LoadFromFile(differential_filename, total_filename); + SetInteractionType(interaction); + SetQuarkType(quark_type); + InitializeSignatures(); + SetUnits(units); +} + +QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types) { + normalize_pdf(); + compute_cdf(); + LoadFromFile(differential_filename, total_filename); + ReadParamsFromSplineTable(); + InitializeSignatures(); + SetUnits(units); +} + +QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, int quark_type, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), quark_type_(quark_type), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { + normalize_pdf(); + compute_cdf(); + LoadFromFile(differential_filename, total_filename); + SetInteractionType(interaction); + SetQuarkType(quark_type); + InitializeSignatures(); + SetUnits(units); +} + +QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()) { + normalize_pdf(); + compute_cdf(); + LoadFromFile(differential_filename, total_filename); + ReadParamsFromSplineTable(); + InitializeSignatures(); + SetUnits(units); +} + +void QuarkDISFromSpline::SetUnits(std::string units) { + std::transform(units.begin(), units.end(), units.begin(), + [](unsigned char c){ return std::tolower(c); }); + if(units == "cm") { + unit = 1.0; + } else if(units == "m") { + unit = 10000.0; + } else { + throw std::runtime_error("Cross section units not supported!"); + } +} + +void QuarkDISFromSpline::SetInteractionType(int interaction) { + interaction_type_ = interaction; +} + +void QuarkDISFromSpline::SetQuarkType(int q_type) { + quark_type_ = q_type; +} + +bool QuarkDISFromSpline::equal(CrossSection const & other) const { + const QuarkDISFromSpline* x = dynamic_cast(&other); + // to do: include more features in the hadronization side to check equivalence + if(!x) + return false; + else + return + std::tie( + interaction_type_, + target_mass_, + minimum_Q2_, + signatures_, + primary_types_, + target_types_, + differential_cross_section_, + total_cross_section_) + == + std::tie( + x->interaction_type_, + x->target_mass_, + x->minimum_Q2_, + x->signatures_, + x->primary_types_, + x->target_types_, + x->differential_cross_section_, + x->total_cross_section_); +} + +void QuarkDISFromSpline::LoadFromFile(std::string dd_crossSectionFile, std::string total_crossSectionFile) { + + differential_cross_section_ = photospline::splinetable<>(dd_crossSectionFile.c_str()); + + if(differential_cross_section_.get_ndim()!=3 && differential_cross_section_.get_ndim()!=2) + throw std::runtime_error("cross section spline has " + std::to_string(differential_cross_section_.get_ndim()) + + " dimensions, should have either 3 (log10(E), log10(x), log10(y)) or 2 (log10(E), log10(y))"); + + total_cross_section_ = photospline::splinetable<>(total_crossSectionFile.c_str()); + + if(total_cross_section_.get_ndim() != 1) + throw std::runtime_error("Total cross section spline has " + std::to_string(total_cross_section_.get_ndim()) + + " dimensions, should have 1, log10(E)"); +} + +void QuarkDISFromSpline::LoadFromMemory(std::vector & differential_data, std::vector & total_data) { + differential_cross_section_.read_fits_mem(differential_data.data(), differential_data.size()); + total_cross_section_.read_fits_mem(total_data.data(), total_data.size()); +} + +double QuarkDISFromSpline::GetLeptonMass(siren::dataclasses::ParticleType lepton_type) { + int32_t lepton_number = std::abs(static_cast(lepton_type)); + double lepton_mass; + switch(lepton_number) { + case 11: + lepton_mass = siren::utilities::Constants::electronMass; + break; + case 13: + lepton_mass = siren::utilities::Constants::muonMass; + break; + case 15: + lepton_mass = siren::utilities::Constants::tauMass; + break; + case 12: + lepton_mass = 0; + case 14: + lepton_mass = 0; + case 16: + lepton_mass = 0; + break; + default: + throw std::runtime_error("Unknown lepton type!"); + } + return lepton_mass; +} + +double QuarkDISFromSpline::getHadronMass(siren::dataclasses::ParticleType hadron_type) { + switch(hadron_type){ + case siren::dataclasses::ParticleType::D0: + return( siren::utilities::Constants::D0Mass); + case siren::dataclasses::ParticleType::D0Bar: + return( siren::utilities::Constants::D0Mass); + case siren::dataclasses::ParticleType::DPlus: + return( siren::utilities::Constants::DPlusMass); + case siren::dataclasses::ParticleType::DMinus: + return( siren::utilities::Constants::DPlusMass); + case siren::dataclasses::ParticleType::Charm: + return( siren::utilities::Constants::CharmMass); + case siren::dataclasses::ParticleType::CharmBar: + return( siren::utilities::Constants::CharmMass); + default: + return(0.0); + } +} + + +std::map QuarkDISFromSpline::getIndices(siren::dataclasses::InteractionSignature signature) { + int lepton_id, hadron_id, meson_id; + for (size_t i = 0; i < signature.secondary_types.size(); i++){ + if (isLepton(signature.secondary_types[i])) { + lepton_id = i; + continue; + } else if (isD(signature.secondary_types[i])) { + meson_id = i; + continue; + } else { + hadron_id = i; + continue; + } + } + return {{"lepton", lepton_id}, {"hadron", hadron_id}, {"meson", meson_id}}; +} + + +void QuarkDISFromSpline::ReadParamsFromSplineTable() { + // returns true if successfully read target mass + bool mass_good = differential_cross_section_.read_key("TARGETMASS", target_mass_); + if (mass_good) {std::cout << "read target mass!!" << std::endl;} // for debugging purposes + // returns true if successfully read interaction type + bool int_good = differential_cross_section_.read_key("INTERACTION", interaction_type_); + // returns true if successfully read minimum Q2 + bool q2_good = differential_cross_section_.read_key("Q2MIN", minimum_Q2_); + // returns true if successfully read quark type + bool qtype_good = differential_cross_section_.read_key("QUARKTYPE", quark_type_); + + + if(!int_good) { + // assume DIS to preserve compatability with previous versions + interaction_type_ = 1; + } + + if (!qtype_good) { + quark_type_ = 1; // assume quark is produced + } + + if(!q2_good) { + // assume 1 GeV^2 + minimum_Q2_ = 1; + } + + if(!mass_good) { + if(int_good) { + if(interaction_type_ == 1 or interaction_type_ == 2) { + target_mass_ = (siren::dataclasses::isLepton(siren::dataclasses::ParticleType::PPlus)+ + siren::dataclasses::isLepton(siren::dataclasses::ParticleType::Neutron))/2; + } else if(interaction_type_ == 3) { + target_mass_ = siren::dataclasses::isLepton(siren::dataclasses::ParticleType::EMinus); + } else { + throw std::runtime_error("Logic error. Interaction type is not 1, 2, or 3!"); + } + + } else { + if(differential_cross_section_.get_ndim() == 3) { + target_mass_ = siren::utilities::Constants::isoscalarMass; + + } else if(differential_cross_section_.get_ndim() == 2) { + target_mass_ = siren::utilities::Constants::electronMass; + } else { + throw std::runtime_error("Logic error. Spline dimensionality is not 2, or 3!"); + } + } + } +} + +void QuarkDISFromSpline::InitializeSignatures() { + signatures_.clear(); + for(auto primary_type : primary_types_) { + dataclasses::InteractionSignature signature; + signature.primary_type = primary_type; + if(not isNeutrino(primary_type)) { + throw std::runtime_error("This DIS implementation only supports neutrinos as primaries!"); + } + // first push back the charged lepton product + siren::dataclasses::ParticleType charged_lepton_product = siren::dataclasses::ParticleType::unknown; + siren::dataclasses::ParticleType neutral_lepton_product = primary_type; + if(primary_type == siren::dataclasses::ParticleType::NuE) { + charged_lepton_product = siren::dataclasses::ParticleType::EMinus; + } else if(primary_type == siren::dataclasses::ParticleType::NuEBar) { + charged_lepton_product = siren::dataclasses::ParticleType::EPlus; + } else if(primary_type == siren::dataclasses::ParticleType::NuMu) { + charged_lepton_product = siren::dataclasses::ParticleType::MuMinus; + } else if(primary_type == siren::dataclasses::ParticleType::NuMuBar) { + charged_lepton_product = siren::dataclasses::ParticleType::MuPlus; + } else if(primary_type == siren::dataclasses::ParticleType::NuTau) { + charged_lepton_product = siren::dataclasses::ParticleType::TauMinus; + } else if(primary_type == siren::dataclasses::ParticleType::NuTauBar) { + charged_lepton_product = siren::dataclasses::ParticleType::TauPlus; + } else { + throw std::runtime_error("InitializeSignatures: Unkown parent neutrino type!"); + } + if(interaction_type_ == 1) { + signature.secondary_types.push_back(charged_lepton_product); + } else if(interaction_type_ == 2) { + signature.secondary_types.push_back(neutral_lepton_product); + } else if(interaction_type_ == 3) { + signature.secondary_types.push_back(siren::dataclasses::ParticleType::Hadrons); + } else { + throw std::runtime_error("InitializeSignatures: Unkown interaction type!"); + } + // now push back the hadron product + signature.secondary_types.push_back(siren::dataclasses::ParticleType::Hadrons); + // define the charmed meson types based on the quark type, now considering only D0 and D+ + if (quark_type_ == 1) { + D_types_ = {siren::dataclasses::Particle::ParticleType::D0, + siren::dataclasses::Particle::ParticleType::DPlus}; + } else { + D_types_ = {siren::dataclasses::Particle::ParticleType::D0Bar, + siren::dataclasses::Particle::ParticleType::DMinus}; + } + // push back the meson type + for (auto meson_type : D_types_) { + dataclasses::InteractionSignature full_signature = signature; + full_signature.secondary_types.push_back(meson_type); + // and finally set the target type and push back the entire signature as well as sig by target + for(auto target_type : target_types_) { + full_signature.target_type = target_type; + + signatures_.push_back(full_signature); + + std::pair key(primary_type, target_type); + signatures_by_parent_types_[key].push_back(full_signature); + } + } + } +} + +void QuarkDISFromSpline::normalize_pdf() { + if (fragmentation_integral == 0){ + std::function integrand = [&] (double x) -> double { + return (0.8 / x ) / (std::pow(1 - (1 / x) - (0.2 / (1 - x)), 2)); + }; + fragmentation_integral = siren::utilities::rombergIntegrate(integrand, 0.001, 0.999); + } else { + std::cout << "Something is wrong... you already computed the normalization" << std::endl; + return; + } +} + +void QuarkDISFromSpline::compute_cdf() { + // first set the z nodes + std::vector zspline; + for (int i = 0; i < 100; ++i) { + zspline.push_back(0.01 + i * (0.99-0.01) / 100 ); + } + + // declare the cdf vectors + std::vector cdf_vector; + std::vector cdf_z_nodes; + std::vector pdf_vector; + + cdf_z_nodes.push_back(0); + cdf_vector.push_back(0); + pdf_vector.push_back(0); + + // compute the spline table + for (int i = 0; i < zspline.size(); ++i) { + if (i == 0) { + double cur_z = zspline[i]; + double cur_pdf = sample_pdf(cur_z); + double area = cur_z * cur_pdf * 0.5; + pdf_vector.push_back(cur_pdf); + cdf_vector.push_back(area); + cdf_z_nodes.push_back(cur_z); + continue; + } + double cur_z = zspline[i]; + double cur_pdf = sample_pdf(cur_z); + double area = 0.5 * (pdf_vector[i - 1] + cur_pdf) * (zspline[i] - zspline[i - 1]); + pdf_vector.push_back(cur_pdf); + cdf_z_nodes.push_back(cur_z); + cdf_vector.push_back(area + cdf_vector.back()); + } + + cdf_z_nodes.push_back(1); + cdf_vector.push_back(1); + pdf_vector.push_back(0); + + + // set the spline table + siren::utilities::TableData1D inverse_cdf_data; + inverse_cdf_data.x = cdf_vector; + inverse_cdf_data.f = cdf_z_nodes; + + inverseCdfTable = siren::utilities::Interpolator1D(inverse_cdf_data); + + return; +} + +double QuarkDISFromSpline::sample_pdf(double x) const { + return (0.8 / x ) / (std::pow(1 - (1 / x) - (0.2 / (1 - x)), 2)) / fragmentation_integral; +} + +double QuarkDISFromSpline::TotalCrossSection(dataclasses::InteractionRecord const & interaction) const { + siren::dataclasses::ParticleType primary_type = interaction.signature.primary_type; + rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); + double primary_energy; + primary_energy = interaction.primary_momentum[0]; + // if we are below threshold, return 0 + if(primary_energy < InteractionThreshold(interaction)) { + std::cout << "DIS::interaction threshold not satisfied" << std::endl; + return 0; + } + return TotalCrossSection(primary_type, primary_energy); +} + +double QuarkDISFromSpline::TotalCrossSection(siren::dataclasses::ParticleType primary_type, double primary_energy) const { + if(not primary_types_.count(primary_type)) { + throw std::runtime_error("Supplied primary not supported by cross section!"); + } + double log_energy = log10(primary_energy); + + if(log_energy < total_cross_section_.lower_extent(0) + or log_energy > total_cross_section_.upper_extent(0)) { + throw std::runtime_error("Interaction energy ("+ std::to_string(primary_energy) + + ") out of cross section table range: [" + + std::to_string(pow(10.,total_cross_section_.lower_extent(0))) + " GeV," + + std::to_string(pow(10.,total_cross_section_.upper_extent(0))) + " GeV]"); + } + + int center; + total_cross_section_.searchcenters(&log_energy, ¢er); + + double log_xs = total_cross_section_.ndsplineeval(&log_energy, ¢er, 0); + if (std::pow(10.0, log_xs) == 0) { + std::cout << "DIS::cross section evaluated to 0" << std::endl; + } + + return unit * std::pow(10.0, log_xs); +} + +double QuarkDISFromSpline::DifferentialCrossSection(dataclasses::InteractionRecord const & interaction) const { + rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); + rk::P4 p2(geom3::Vector3(0, 0, 0), interaction.target_mass); + double primary_energy; + primary_energy = interaction.primary_momentum[0]; + assert(interaction.signature.secondary_types.size() == 3); + std::map secondaries = getIndices(interaction.signature); + unsigned int lepton_index = secondaries["lepton"]; + unsigned int hadron_index = secondaries["hadron"]; + unsigned int meson_index = secondaries["meson"]; + + std::array const & mom3 = interaction.secondary_momenta[lepton_index]; + std::array const & mom_x = interaction.secondary_momenta[hadron_index]; + std::array const & mom_d = interaction.secondary_momenta[meson_index]; + + rk::P4 p3(geom3::Vector3(mom3[1], mom3[2], mom3[3]), interaction.secondary_masses[lepton_index]); + rk::P4 p_x(geom3::Vector3(mom_x[1], mom_x[2], mom_x[3]), interaction.secondary_masses[hadron_index]); + rk::P4 p_d(geom3::Vector3(mom_d[1], mom_d[2], mom_d[3]), interaction.secondary_masses[meson_index]); + rk::P4 p4 = p_x + p_d; // this assume that we are working in a good frame where the hadronization vertex has 4-momentum conserved + rk::P4 q = p1 - p3; + // however p4 is not used in computation here so we should be fine... + + double Q2 = -q.dot(q); + double x, y; + double lepton_mass = GetLeptonMass(interaction.signature.secondary_types[lepton_index]); + + y = 1.0 - p2.dot(p3) / p2.dot(p1); + x = Q2 / (2.0 * p2.dot(q)); + double log_energy = log10(primary_energy); + std::array coordinates{{log_energy, log10(x), log10(y)}}; + std::array centers; + + if (Q2 < minimum_Q2_ || !kinematicallyAllowed(x, y, primary_energy, target_mass_, lepton_mass) + || !differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { + // std::cout << "weighting: revert back to saved x and y" << std::endl; + double E1_lab = interaction.interaction_parameters.at("energy"); + double E2_lab = p2.e(); + x = interaction.interaction_parameters.at("bjorken_x"); + y = interaction.interaction_parameters.at("bjorken_y"); + Q2 = 2. * E1_lab * E2_lab * x * y; + } + return DifferentialCrossSection(primary_energy, x, y, lepton_mass, Q2); +} + +double QuarkDISFromSpline::DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2) const { + double log_energy = log10(energy); + // check preconditions + if(log_energy < differential_cross_section_.lower_extent(0) + || log_energy>differential_cross_section_.upper_extent(0)) + {std::cout << "Diff xsec: not in bounds" << std::endl; + return 0.0;} + if(x <= 0 || x >= 1) { + std::cout << "x is out of bounds with x = " << x << std::endl; + return 0.0; + } + if(y <= 0 || y >= 1){ + std::cout << "y is out of bounds with x = " << y << std::endl; + return 0.0; + } + + if(std::isnan(Q2)) { + Q2 = 2.0 * energy * target_mass_ * x * y; + } + if(Q2 < minimum_Q2_) { + std::cout << "Q2 is smaller than minimum Q2 with " << Q2 << " < " << minimum_Q2_ << std::endl; + return 0; + } // cross section not calculated, assumed to be zero + + if(!kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) { + std::cout << "not kinematically allowed!" << std::endl; + return 0; + } + std::array coordinates{{log_energy, log10(x), log10(y)}}; + std::array centers; + if(!differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { + std::cout << "search centers failed!" << std::endl; + return 0; + } + double result = pow(10., differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0)); + assert(result >= 0); + if (std::isinf(result)) { + std::cout << "energy, x, y, Q2 are " << energy << " " << x << " " << y << " " << Q2 << " " << std::endl; + std::cout << "spline value read is " << differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0) << std::endl; + } + return unit * result; +} + +double QuarkDISFromSpline::InteractionThreshold(dataclasses::InteractionRecord const & interaction) const { + // Consider implementing DIS thershold at some point + return 0; +} + +void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { + // first obtain the indices from secondaries + // std::cout << "in sample final state" << std::endl; + std::map secondary_indices = getIndices(record.signature); + unsigned int lepton_index = secondary_indices["lepton"]; + unsigned int hadron_index = secondary_indices["hadron"]; + unsigned int meson_index = secondary_indices["meson"]; + + // Uses Metropolis-Hastings Algorithm! + // useful for cases where we don't know the supremum of our distribution, and the distribution is multi-dimensional + if (differential_cross_section_.get_ndim() != 3) { + throw std::runtime_error("I expected 3 dimensions in the cross section spline, but got " + std::to_string(differential_cross_section_.get_ndim()) +". Maybe your fits file doesn't have the right 'INTERACTION' key?"); + } + rk::P4 p1(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); + // std::cout << "quark::sampleFinalState : primary momentum is read to be " << p1 << std::endl; + rk::P4 p2(geom3::Vector3(0, 0, 0), target_mass_); + + // we assume that: + // the target is stationary so its energy is just its mass + // the incoming neutrino is massless, so its kinetic energy is its total energy + // double s = target_mass_ * trecord.secondary_momentarget_mass_ + 2 * target_mass_ * primary_energy; + // double s = std::pow(rk::invMass(p1, p2), 2); + + double primary_energy; + rk::P4 p1_lab; + rk::P4 p2_lab; + p1_lab = p1; + p2_lab = p2; + primary_energy = p1_lab.e(); + + // correctly assign lepton, hadron and meson index + double m = GetLeptonMass(record.signature.secondary_types[lepton_index]); + + double m1 = record.primary_mass; + double m3 = m; + double E1_lab = p1_lab.e(); + double E2_lab = p2_lab.e(); + + // The out-going particle always gets at least enough energy for its rest mass + double yMax = 1 - m / primary_energy; + double logYMax = log10(yMax); + + // The minimum allowed value of y occurs when x = 1 and Q is minimized + double yMin = minimum_Q2_ / (2 * E1_lab * E2_lab); + double logYMin = log10(yMin); + // The minimum allowed value of x occurs when y = yMax and Q is minimized + // double xMin = minimum_Q2_ / ((s - target_mass_ * target_mass_) * yMax); + double xMin = minimum_Q2_ / (2 * E1_lab * E2_lab * yMax); + double logXMin = log10(xMin); + + bool accept; + + // kin_vars and its twin are 3-vectors containing [nu-energy, Bjorken X, Bjorken Y] + std::array kin_vars, test_kin_vars; + + // centers of the cross section spline tales. + std::array spline_table_center, test_spline_table_center; + + // values of cross_section from the splines. By * Bx * Spline(E,x,y) + double cross_section, test_cross_section; + + // No matter what, we're evaluating at this specific energy. + kin_vars[0] = test_kin_vars[0] = log10(primary_energy); + + // check preconditions + if(kin_vars[0] < differential_cross_section_.lower_extent(0) + || kin_vars[0] > differential_cross_section_.upper_extent(0)) + throw std::runtime_error("Interaction energy out of cross section table range: [" + + std::to_string(pow(10.,differential_cross_section_.lower_extent(0))) + " GeV," + + std::to_string(pow(10.,differential_cross_section_.upper_extent(0))) + " GeV]"); + + // sample an intial point + do { + // rejection sample a point which is kinematically allowed by calculation limits + double trialQ; + double trials = 0; + do { + if (trials >= 100) throw std::runtime_error("too much trials"); + trials += 1; + kin_vars[1] = random->Uniform(logXMin,0); + kin_vars[2] = random->Uniform(logYMin,logYMax); + trialQ = (2 * E1_lab * E2_lab) * pow(10., kin_vars[1] + kin_vars[2]); + } while(trialQ differential_cross_section_.upper_extent(1)) { + accept = false; + } + if(kin_vars[2] < differential_cross_section_.lower_extent(2) + || kin_vars[2] > differential_cross_section_.upper_extent(2)) { + accept = false; + } + + if(accept) { + // finds the centers in the cross section spline table, returns true if it's successful + // also sets the centers + accept = differential_cross_section_.searchcenters(kin_vars.data(),spline_table_center.data()); + } + } while(!accept); + + //TODO: better proposal distribution? + double measure = pow(10., kin_vars[1] + kin_vars[2]); // Bx * By + + // Bx * By * xs(E, x, y) + // evalutates the differential spline at that point + cross_section = measure*pow(10., differential_cross_section_.ndsplineeval(kin_vars.data(), spline_table_center.data(), 0)); + + // this is the magic part. Metropolis Hastings Algorithm. + // MCMC method! + const size_t burnin = 40; // converges to the correct distribution over multiple samplings. + // big number means more accurate, but slower + for(size_t j = 0; j <= burnin; j++) { + // repeat the sampling from above to get a new valid point + double trialQ; + do { + test_kin_vars[1] = random->Uniform(logXMin, 0); + test_kin_vars[2] = random->Uniform(logYMin, logYMax); + trialQ = (2 * E1_lab * E2_lab) * pow(10., test_kin_vars[1] + test_kin_vars[2]); + } while(trialQ < minimum_Q2_ || !kinematicallyAllowed(pow(10., test_kin_vars[1]), pow(10., test_kin_vars[2]), primary_energy, target_mass_, m)); + + accept = true; + if(test_kin_vars[1] < differential_cross_section_.lower_extent(1) + || test_kin_vars[1] > differential_cross_section_.upper_extent(1)) + accept = false; + if(test_kin_vars[2] < differential_cross_section_.lower_extent(2) + || test_kin_vars[2] > differential_cross_section_.upper_extent(2)) + accept = false; + if(!accept) + continue; + + accept = differential_cross_section_.searchcenters(test_kin_vars.data(), test_spline_table_center.data()); + if(!accept) + continue; + + double measure = pow(10., test_kin_vars[1] + test_kin_vars[2]); + double eval = differential_cross_section_.ndsplineeval(test_kin_vars.data(), test_spline_table_center.data(), 0); + if(std::isnan(eval)) + continue; + test_cross_section = measure * pow(10., eval); + + double odds = (test_cross_section / cross_section); + accept = (cross_section == 0 || (odds > 1.) || random->Uniform(0, 1) < odds); + + if(accept) { + kin_vars = test_kin_vars; + cross_section = test_cross_section; + // std::cout << "trial Q is" << trialQ << std::endl; + } + } + + // scaling down to handle numerical issues + double final_x = pow(10., kin_vars[1]); + double final_y = pow(10., kin_vars[2]); + record.interaction_parameters.clear(); + record.interaction_parameters["energy"] = E1_lab; + record.interaction_parameters["bjorken_x"] = final_x; + record.interaction_parameters["bjorken_y"] = final_y; + + double Q2 = 2 * E1_lab * E2_lab * pow(10.0, kin_vars[1] + kin_vars[2]); + double p1x_lab = std::sqrt(p1_lab.px() * p1_lab.px() + p1_lab.py() * p1_lab.py() + p1_lab.pz() * p1_lab.pz()); + double pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); + double momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); + double pqy_lab, Eq_lab; + + if (pqx_lab>momq_lab){ + // if current setting does not work, start looping through scalings + int maxIterations = 10; + int iteration = 0; + double p1_lab_x = p1_lab.px(); + double p1_lab_y = p1_lab.py(); + double p1_lab_z = p1_lab.pz(); + // loop to resolve precision issue + while (iteration <= maxIterations) { + Q2 = 2. * E1_lab * E2_lab * pow(10.0, kin_vars[1] + kin_vars[2]); + p1x_lab = std::sqrt(p1_lab_x * p1_lab_x + p1_lab_y * p1_lab_y + p1_lab_z * p1_lab_z); + pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); + momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); + if (pqx_lab>momq_lab){ + // std::cout << "triggered on " << momq_lab << " and " << pqx_lab << std::endl; + //scale down + E1_lab /= 10; + E2_lab /= 10; + p1_lab_x /= 10; + p1_lab_y /= 10; + p1_lab_z /= 10; + m1 /= 10; + m3 /= 10; + //iteration += 1 to scale back + iteration += 1; + continue; + } + pqy_lab = std::sqrt((momq_lab + pqx_lab) * (momq_lab - pqx_lab)); + // std::cout << "finished with " << iteration << " iterations and " << momq_lab << " and " << pqx_lab << std::endl; + break; + } + // //scale back + if (iteration > 0) { + // std::cout << "scaling back with " << pow(10.0, iteration); + E1_lab *= pow(10.0, iteration); + E2_lab *= pow(10.0, iteration); + p1_lab_x *= pow(10.0, iteration); + p1_lab_y *= pow(10.0, iteration); + p1_lab_z *= pow(10.0, iteration); + m1 *= pow(10.0, iteration); + m3 *= pow(10.0, iteration); + // std::cout << "and finished with " << momq_lab << " and " << pqx_lab << std::endl; + } + // pqy_lab = 0; + } else {pqy_lab = std::sqrt(momq_lab*momq_lab - pqx_lab *pqx_lab);} + Eq_lab = E1_lab * final_y; + + geom3::UnitVector3 x_dir = geom3::UnitVector3::xAxis(); + geom3::Vector3 p1_mom = p1_lab.momentum(); + geom3::UnitVector3 p1_lab_dir = p1_mom.direction(); + geom3::Rotation3 x_to_p1_lab_rot = geom3::rotationBetween(x_dir, p1_lab_dir); + + double phi = random->Uniform(0, 2.0 * M_PI); + geom3::Rotation3 rand_rot(p1_lab_dir, phi); + + rk::P4 pq_lab(Eq_lab, geom3::Vector3(pqx_lab, pqy_lab, 0)); + pq_lab.rotate(x_to_p1_lab_rot); + pq_lab.rotate(rand_rot); + + rk::P4 p3_lab((p1_lab - pq_lab).momentum(), m3); + rk::P4 p4_lab = p2_lab + pq_lab; + + rk::P4 p3; + rk::P4 p4; + p3 = p3_lab; // now we have our lepton momentum set, which should not be modified from here on + p4 = p4_lab; // momentum of the virtual charm + // std::cout << "charm momentum is " << p4 << std::endl; + + // compute the energy and 3-momentum of the virtual charm + // std::cout << "the virtual charm off-shell mass is " << p4.m() << std::endl; + double p3c = std::sqrt(std::pow(p4.px(), 2) + std::pow(p4.py(), 2) + std::pow(p4.pz(), 2)); + double Ec = p4.e(); //energy of primary charm + double mCH = getHadronMass(record.signature.secondary_types[meson_index]); // obtain charmed hadron mass + + // accept-reject sampling for a valid momentum fragmentation + bool frag_accept; + double randValue; + double z; + double ECH; + + // add a maximum number of trials in the while loop + int max_sampling = 500; + int sampling = 0; + + // sample again if this eenrgy is not kinematically allowed + // this samples in the lab frame the energy of the D-meson such that mass is real + do { + sampling += 1; + if (sampling > max_sampling) { + std::cout << "energy of the charm is " << Ec << " and momentum is " << p3c << std::endl; + std::cout << "desired mass of hadron is " << mCH << std::endl; + // throw(siren::utilities::InjectionFailure("Failed to sample hadronization!")); + break; + } + randValue = random->Uniform(0,1); + z = inverseCdfTable(randValue); + ECH = z * Ec; + if (std::pow(ECH, 2) - std::pow(mCH, 2) <= 0) { + frag_accept = false; + } else { + frag_accept = true; + } + } while (!frag_accept); + // new attempt of using the isoscalar mass as the remnant hadronic shower mass + double mX = target_mass_; + double Mc = p4.m(); + // std::cout << "using remnant mass " << mX << std::endl; + // std::cout << "invariant charm mass and its energy is " << Mc << ", " << p4.e() << std::endl; + // std::cout << "target sampled D meson energy is " << ECH << std::endl; + // std::cout << "and the fraction of momentum is sampled to be " << z << std::endl; + //compute the energies in the charm rest frame + double E_CH_c = (std::pow(Mc, 2) - std::pow(mX, 2) + std::pow(mCH, 2)) / (2 * Mc); + // std::cout << "energy of charm in rest frame is " << E_CH_c << std::endl; + double p_c = std::sqrt((std::pow(Mc, 2) - std::pow(mCH + mX, 2)) * (std::pow(Mc, 2) - std::pow(mCH - mX, 2))) / (2 * Mc); + // std::cout << "momentum in charm rest frame is " << p_c << std::endl; + // compute the lorentz boost parameters + double gamma = p4.gamma(); + double beta = p4.beta(); + // std::cout << "beta and gamma parameters are " << beta << ", " << gamma << std::endl; + // using the lab frame fragmented energy and the + double cosTheta = std::max(std::min(((ECH - gamma * E_CH_c)/(gamma * beta * p_c)), 1.), -1.); + // std::cout << "cosine of theta in charm frame is " << cosTheta << std::endl; + // std::cout << "without cutting, the number is " << (ECH - gamma * E_CH_c)/(gamma * beta * p_c) << std::endl; + // now compute the momentum vectors in the rest frame + double sinTheta = std::sin(std::acos(cosTheta)); + // std::cout << "and sine of theta is computed to be " << sinTheta << std::endl; + rk::P4 p4CH_c(p_c * geom3::Vector3(cosTheta, sinTheta, 0), mCH); + rk::P4 p4X_c(p_c * geom3::Vector3(-cosTheta, -sinTheta, 0), mX); + // these all assume boost direction is charm direction. Now we should rotate back to charm lab momentum direction + geom3::Vector3 pc_lab_momentum = p4.momentum(); + geom3::UnitVector3 pc_lab_dir = pc_lab_momentum.direction(); + geom3::Rotation3 x_to_pc_lab_rot = geom3::rotationBetween(x_dir, pc_lab_dir); + p4X_c.rotate(x_to_pc_lab_rot); + p4CH_c.rotate(x_to_pc_lab_rot); + + // finally, we perform a random azimuthal rotation + double c_phi = random->Uniform(0, 2 * M_PI); + geom3::Rotation3 azimuth_rand_rot(pc_lab_dir, c_phi); + p4X_c.rotate(azimuth_rand_rot); + p4CH_c.rotate(azimuth_rand_rot); + + // and boost them back to the lab frame + rk::Boost boost_from_crest_to_lab = p4.labBoost(); + rk::P4 p4X = p4X_c.boost(boost_from_crest_to_lab); + rk::P4 p4CH = p4CH_c.boost(boost_from_crest_to_lab); + + // std::cout << "computed remnant mass and energy is " << p4X.m() << ", " << p4X.e() << std::endl; + // std::cout << "and computed D mass and energy is " << p4CH.m() << ", " << p4CH.e() << std::endl; + // std::cout << "target sampled D meson energy is " << ECH << std::endl; + + + // now we proceed to saving the final state kinematics + std::vector & secondaries = record.GetSecondaryParticleRecords(); + siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[lepton_index]; + siren::dataclasses::SecondaryParticleRecord & hadron = secondaries[hadron_index]; + siren::dataclasses::SecondaryParticleRecord & meson = secondaries[meson_index]; + // std::cout << "QuarkDIS::SampleFInalState : the indices are: " << lepton_index << hadron_index<< meson_index << std::endl; + + lepton.SetFourMomentum({p3.e(), p3.px(), p3.py(), p3.pz()}); + // std::cout << "setting lepton mass with lepton momentum " << p3 << std::endl; + lepton.SetMass(p3.m()); + lepton.SetHelicity(record.primary_helicity); + hadron.SetFourMomentum({p4X.e(), p4X.px(), p4X.py(), p4X.pz()}); + // std::cout << "setting hadron mass with hadron momentum " << p4X << std::endl; + hadron.SetMass(p4X.m()); + hadron.SetHelicity(record.target_helicity); + meson.SetFourMomentum({p4CH.e(), p4CH.px(), p4CH.py(), p4CH.pz()}); + // std::cout << "setting meson mass with meson momentum " << p4CH << std::endl; + meson.SetMass(p4CH.m()); + meson.SetHelicity(record.target_helicity); // this needs working on + // std::cout << "finished sampling final state" << std::endl; +} + +double QuarkDISFromSpline::FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const { + if (secondary == siren::dataclasses::Particle::ParticleType::D0 || secondary == siren::dataclasses::Particle::ParticleType::D0Bar) { + return 0.6; + } else if (secondary == siren::dataclasses::Particle::ParticleType::DPlus || secondary == siren::dataclasses::Particle::ParticleType::DMinus) { + return 0.23; + } // D_s and Lambda^+ not yet implemented + return 0; +} + +double QuarkDISFromSpline::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { + // first compute the differential and total cross section + double dxs = DifferentialCrossSection(interaction); + // if (dxs == 0) { + // std::cout << "diff xsec gives 0" << std::endl; + // } + double txs = TotalCrossSection(interaction); + //then compute the fragmentation probability + std::map secondaries = getIndices(interaction.signature); + unsigned int meson_index = secondaries["meson"]; + double fragfrac = FragmentationFraction(interaction.signature.secondary_types[meson_index]); + if(dxs == 0) { + std::cout << "diff xsec gives 0" << std::endl; + return 0.0; + } else { + // if (txs == 0) {std::cout << "wtf??? txs is 0 in final state prob" << txs << std::endl;} + if (std::isinf(dxs)) {std::cout << "dxs is inf in final state prob" << std::endl;} + return dxs / txs * fragfrac; + } +} + +std::vector QuarkDISFromSpline::GetPossiblePrimaries() const { + return std::vector(primary_types_.begin(), primary_types_.end()); +} + +std::vector QuarkDISFromSpline::GetPossibleTargetsFromPrimary(siren::dataclasses::ParticleType primary_type) const { + return std::vector(target_types_.begin(), target_types_.end()); +} + +std::vector QuarkDISFromSpline::GetPossibleSignatures() const { + return std::vector(signatures_.begin(), signatures_.end()); +} + +std::vector QuarkDISFromSpline::GetPossibleTargets() const { + return std::vector(target_types_.begin(), target_types_.end()); +} + +std::vector QuarkDISFromSpline::GetPossibleSignaturesFromParents(siren::dataclasses::ParticleType primary_type, siren::dataclasses::ParticleType target_type) const { + std::pair key(primary_type, target_type); + if(signatures_by_parent_types_.find(key) != signatures_by_parent_types_.end()) { + return signatures_by_parent_types_.at(key); + } else { + return std::vector(); + } +} + +std::vector QuarkDISFromSpline::DensityVariables() const { + return std::vector{"Bjorken x", "Bjorken y"}; +} + +} // namespace interactions +} // namespace siren From ddf7d2c38e23523162827f144602a60660e0d0f0 Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Wed, 25 Mar 2026 00:46:06 -0400 Subject: [PATCH 16/93] fixed TotalCrossSection for QuarkDISFromSpline to take signature and avoid double counting --- .../private/QuarkDISFromSpline.cxx.bak | 972 ------------------ 1 file changed, 972 deletions(-) delete mode 100644 projects/interactions/private/QuarkDISFromSpline.cxx.bak diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx.bak b/projects/interactions/private/QuarkDISFromSpline.cxx.bak deleted file mode 100644 index fa5b091fb..000000000 --- a/projects/interactions/private/QuarkDISFromSpline.cxx.bak +++ /dev/null @@ -1,972 +0,0 @@ -#include "SIREN/interactions/QuarkDISFromSpline.h" - -#include // for map, opera... -#include // for set, opera... -#include // for array -#include // for pow, log10 -#include // for tie, opera... -#include // for allocator -#include // for basic_string -#include // for vector -#include // for assert -#include // for size_t -#include -#include -#include -#include -#include - -#include // for P4, Boost -#include // for Vector3 - -#include // for splinetable -//#include - -#include "SIREN/interactions/CrossSection.h" // for CrossSection -#include "SIREN/dataclasses/InteractionRecord.h" // for Interactio... -#include "SIREN/dataclasses/Particle.h" // for Particle -#include "SIREN/utilities/Random.h" // for SIREN_random -#include "SIREN/utilities/Constants.h" // for electronMass -#include "SIREN/utilities/Errors.h" // for PythonImplementationError - - -namespace siren { -namespace interactions { - -namespace { -///Check whether a given point in phase space is physically realizable. -///Based on equations 6-8 of http://dx.doi.org/10.1103/PhysRevD.66.113007 -///S. Kretzer and M. H. Reno -///"Tau neutrino deep inelastic charged current interactions" -///Phys. Rev. D 66, 113007 -///\param x Bjorken x of the interaction -///\param y Bjorken y of the interaction -///\param E Incoming neutrino in energy in the lab frame ($E_\nu$) -///\param M Mass of the target nucleon ($M_N$) -///\param m Mass of the secondary lepton ($m_\tau$) -bool kinematicallyAllowed(double x, double y, double E, double M, double m) { - if(x > 1) //Eq. 6 right inequality - return false; - if(x < ((m * m) / (2 * M * (E - m)))) //Eq. 6 left inequality - return false; - if (x < 1e-6 || y < 1e-6) return false; - - //denominator of a and b - double d = 2 * (1 + (M * x) / (2 * E)); - //the numerator of a (or a*d) - double ad = 1 - m * m * ((1 / (2 * M * E * x)) + (1 / (2 * E * E))); - double term = 1 - ((m * m) / (2 * M * E * x)); - //the numerator of b (or b*d) - double bd = sqrt(term * term - ((m * m) / (E * E))); - - double s = 2 * M * E; - double Q2 = s * x * y; - double Mc = siren::utilities::Constants::D0Mass; - return ((ad - bd) <= d * y and d * y <= (ad + bd)) && (Q2 * (1 - x) / x + pow(M, 2) >= pow(M + Mc, 2)); //Eq. 7 -} -} - -QuarkDISFromSpline::QuarkDISFromSpline() { - // initialize the pdf normalization and cdf table for the hadronization process - normalize_pdf(); - compute_cdf(); -} - -QuarkDISFromSpline::QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, int quark_type, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), quark_type_(quark_type), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { - normalize_pdf(); - compute_cdf(); - LoadFromMemory(differential_data, total_data); - SetInteractionType(interaction); - SetQuarkType(quark_type); - InitializeSignatures(); - SetUnits(units); -} - -QuarkDISFromSpline::QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, int quark_type, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction),quark_type_(quark_type), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { - normalize_pdf(); - compute_cdf(); - LoadFromMemory(differential_data, total_data); - SetInteractionType(interaction); - SetQuarkType(quark_type); - InitializeSignatures(); - SetUnits(units); -} - -QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, int quark_type, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), quark_type_(quark_type), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { - normalize_pdf(); - compute_cdf(); - LoadFromFile(differential_filename, total_filename); - SetInteractionType(interaction); - SetQuarkType(quark_type); - InitializeSignatures(); - SetUnits(units); -} - -QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types) { - normalize_pdf(); - compute_cdf(); - LoadFromFile(differential_filename, total_filename); - ReadParamsFromSplineTable(); - InitializeSignatures(); - SetUnits(units); -} - -QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, int quark_type, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), quark_type_(quark_type), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { - normalize_pdf(); - compute_cdf(); - LoadFromFile(differential_filename, total_filename); - SetInteractionType(interaction); - SetQuarkType(quark_type); - InitializeSignatures(); - SetUnits(units); -} - -QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()) { - normalize_pdf(); - compute_cdf(); - LoadFromFile(differential_filename, total_filename); - ReadParamsFromSplineTable(); - InitializeSignatures(); - SetUnits(units); -} - -void QuarkDISFromSpline::SetUnits(std::string units) { - std::transform(units.begin(), units.end(), units.begin(), - [](unsigned char c){ return std::tolower(c); }); - if(units == "cm") { - unit = 1.0; - } else if(units == "m") { - unit = 10000.0; - } else { - throw std::runtime_error("Cross section units not supported!"); - } -} - -void QuarkDISFromSpline::SetInteractionType(int interaction) { - interaction_type_ = interaction; -} - -void QuarkDISFromSpline::SetQuarkType(int q_type) { - quark_type_ = q_type; -} - -bool QuarkDISFromSpline::equal(CrossSection const & other) const { - const QuarkDISFromSpline* x = dynamic_cast(&other); - // to do: include more features in the hadronization side to check equivalence - if(!x) - return false; - else - return - std::tie( - interaction_type_, - target_mass_, - minimum_Q2_, - signatures_, - primary_types_, - target_types_, - differential_cross_section_, - total_cross_section_) - == - std::tie( - x->interaction_type_, - x->target_mass_, - x->minimum_Q2_, - x->signatures_, - x->primary_types_, - x->target_types_, - x->differential_cross_section_, - x->total_cross_section_); -} - -void QuarkDISFromSpline::LoadFromFile(std::string dd_crossSectionFile, std::string total_crossSectionFile) { - - differential_cross_section_ = photospline::splinetable<>(dd_crossSectionFile.c_str()); - - if(differential_cross_section_.get_ndim()!=3 && differential_cross_section_.get_ndim()!=2) - throw std::runtime_error("cross section spline has " + std::to_string(differential_cross_section_.get_ndim()) - + " dimensions, should have either 3 (log10(E), log10(x), log10(y)) or 2 (log10(E), log10(y))"); - - total_cross_section_ = photospline::splinetable<>(total_crossSectionFile.c_str()); - - if(total_cross_section_.get_ndim() != 1) - throw std::runtime_error("Total cross section spline has " + std::to_string(total_cross_section_.get_ndim()) - + " dimensions, should have 1, log10(E)"); -} - -void QuarkDISFromSpline::LoadFromMemory(std::vector & differential_data, std::vector & total_data) { - differential_cross_section_.read_fits_mem(differential_data.data(), differential_data.size()); - total_cross_section_.read_fits_mem(total_data.data(), total_data.size()); -} - -double QuarkDISFromSpline::GetLeptonMass(siren::dataclasses::ParticleType lepton_type) { - int32_t lepton_number = std::abs(static_cast(lepton_type)); - double lepton_mass; - switch(lepton_number) { - case 11: - lepton_mass = siren::utilities::Constants::electronMass; - break; - case 13: - lepton_mass = siren::utilities::Constants::muonMass; - break; - case 15: - lepton_mass = siren::utilities::Constants::tauMass; - break; - case 12: - lepton_mass = 0; - case 14: - lepton_mass = 0; - case 16: - lepton_mass = 0; - break; - default: - throw std::runtime_error("Unknown lepton type!"); - } - return lepton_mass; -} - -double QuarkDISFromSpline::getHadronMass(siren::dataclasses::ParticleType hadron_type) { - switch(hadron_type){ - case siren::dataclasses::ParticleType::D0: - return( siren::utilities::Constants::D0Mass); - case siren::dataclasses::ParticleType::D0Bar: - return( siren::utilities::Constants::D0Mass); - case siren::dataclasses::ParticleType::DPlus: - return( siren::utilities::Constants::DPlusMass); - case siren::dataclasses::ParticleType::DMinus: - return( siren::utilities::Constants::DPlusMass); - case siren::dataclasses::ParticleType::Charm: - return( siren::utilities::Constants::CharmMass); - case siren::dataclasses::ParticleType::CharmBar: - return( siren::utilities::Constants::CharmMass); - default: - return(0.0); - } -} - - -std::map QuarkDISFromSpline::getIndices(siren::dataclasses::InteractionSignature signature) { - int lepton_id, hadron_id, meson_id; - for (size_t i = 0; i < signature.secondary_types.size(); i++){ - if (isLepton(signature.secondary_types[i])) { - lepton_id = i; - continue; - } else if (isD(signature.secondary_types[i])) { - meson_id = i; - continue; - } else { - hadron_id = i; - continue; - } - } - return {{"lepton", lepton_id}, {"hadron", hadron_id}, {"meson", meson_id}}; -} - - -void QuarkDISFromSpline::ReadParamsFromSplineTable() { - // returns true if successfully read target mass - bool mass_good = differential_cross_section_.read_key("TARGETMASS", target_mass_); - if (mass_good) {std::cout << "read target mass!!" << std::endl;} // for debugging purposes - // returns true if successfully read interaction type - bool int_good = differential_cross_section_.read_key("INTERACTION", interaction_type_); - // returns true if successfully read minimum Q2 - bool q2_good = differential_cross_section_.read_key("Q2MIN", minimum_Q2_); - // returns true if successfully read quark type - bool qtype_good = differential_cross_section_.read_key("QUARKTYPE", quark_type_); - - - if(!int_good) { - // assume DIS to preserve compatability with previous versions - interaction_type_ = 1; - } - - if (!qtype_good) { - quark_type_ = 1; // assume quark is produced - } - - if(!q2_good) { - // assume 1 GeV^2 - minimum_Q2_ = 1; - } - - if(!mass_good) { - if(int_good) { - if(interaction_type_ == 1 or interaction_type_ == 2) { - target_mass_ = (siren::dataclasses::isLepton(siren::dataclasses::ParticleType::PPlus)+ - siren::dataclasses::isLepton(siren::dataclasses::ParticleType::Neutron))/2; - } else if(interaction_type_ == 3) { - target_mass_ = siren::dataclasses::isLepton(siren::dataclasses::ParticleType::EMinus); - } else { - throw std::runtime_error("Logic error. Interaction type is not 1, 2, or 3!"); - } - - } else { - if(differential_cross_section_.get_ndim() == 3) { - target_mass_ = siren::utilities::Constants::isoscalarMass; - - } else if(differential_cross_section_.get_ndim() == 2) { - target_mass_ = siren::utilities::Constants::electronMass; - } else { - throw std::runtime_error("Logic error. Spline dimensionality is not 2, or 3!"); - } - } - } -} - -void QuarkDISFromSpline::InitializeSignatures() { - signatures_.clear(); - for(auto primary_type : primary_types_) { - dataclasses::InteractionSignature signature; - signature.primary_type = primary_type; - if(not isNeutrino(primary_type)) { - throw std::runtime_error("This DIS implementation only supports neutrinos as primaries!"); - } - // first push back the charged lepton product - siren::dataclasses::ParticleType charged_lepton_product = siren::dataclasses::ParticleType::unknown; - siren::dataclasses::ParticleType neutral_lepton_product = primary_type; - if(primary_type == siren::dataclasses::ParticleType::NuE) { - charged_lepton_product = siren::dataclasses::ParticleType::EMinus; - } else if(primary_type == siren::dataclasses::ParticleType::NuEBar) { - charged_lepton_product = siren::dataclasses::ParticleType::EPlus; - } else if(primary_type == siren::dataclasses::ParticleType::NuMu) { - charged_lepton_product = siren::dataclasses::ParticleType::MuMinus; - } else if(primary_type == siren::dataclasses::ParticleType::NuMuBar) { - charged_lepton_product = siren::dataclasses::ParticleType::MuPlus; - } else if(primary_type == siren::dataclasses::ParticleType::NuTau) { - charged_lepton_product = siren::dataclasses::ParticleType::TauMinus; - } else if(primary_type == siren::dataclasses::ParticleType::NuTauBar) { - charged_lepton_product = siren::dataclasses::ParticleType::TauPlus; - } else { - throw std::runtime_error("InitializeSignatures: Unkown parent neutrino type!"); - } - if(interaction_type_ == 1) { - signature.secondary_types.push_back(charged_lepton_product); - } else if(interaction_type_ == 2) { - signature.secondary_types.push_back(neutral_lepton_product); - } else if(interaction_type_ == 3) { - signature.secondary_types.push_back(siren::dataclasses::ParticleType::Hadrons); - } else { - throw std::runtime_error("InitializeSignatures: Unkown interaction type!"); - } - // now push back the hadron product - signature.secondary_types.push_back(siren::dataclasses::ParticleType::Hadrons); - // define the charmed meson types based on the quark type, now considering only D0 and D+ - if (quark_type_ == 1) { - D_types_ = {siren::dataclasses::Particle::ParticleType::D0, - siren::dataclasses::Particle::ParticleType::DPlus}; - } else { - D_types_ = {siren::dataclasses::Particle::ParticleType::D0Bar, - siren::dataclasses::Particle::ParticleType::DMinus}; - } - // push back the meson type - for (auto meson_type : D_types_) { - dataclasses::InteractionSignature full_signature = signature; - full_signature.secondary_types.push_back(meson_type); - // and finally set the target type and push back the entire signature as well as sig by target - for(auto target_type : target_types_) { - full_signature.target_type = target_type; - - signatures_.push_back(full_signature); - - std::pair key(primary_type, target_type); - signatures_by_parent_types_[key].push_back(full_signature); - } - } - } -} - -void QuarkDISFromSpline::normalize_pdf() { - if (fragmentation_integral == 0){ - std::function integrand = [&] (double x) -> double { - return (0.8 / x ) / (std::pow(1 - (1 / x) - (0.2 / (1 - x)), 2)); - }; - fragmentation_integral = siren::utilities::rombergIntegrate(integrand, 0.001, 0.999); - } else { - std::cout << "Something is wrong... you already computed the normalization" << std::endl; - return; - } -} - -void QuarkDISFromSpline::compute_cdf() { - // first set the z nodes - std::vector zspline; - for (int i = 0; i < 100; ++i) { - zspline.push_back(0.01 + i * (0.99-0.01) / 100 ); - } - - // declare the cdf vectors - std::vector cdf_vector; - std::vector cdf_z_nodes; - std::vector pdf_vector; - - cdf_z_nodes.push_back(0); - cdf_vector.push_back(0); - pdf_vector.push_back(0); - - // compute the spline table - for (int i = 0; i < zspline.size(); ++i) { - if (i == 0) { - double cur_z = zspline[i]; - double cur_pdf = sample_pdf(cur_z); - double area = cur_z * cur_pdf * 0.5; - pdf_vector.push_back(cur_pdf); - cdf_vector.push_back(area); - cdf_z_nodes.push_back(cur_z); - continue; - } - double cur_z = zspline[i]; - double cur_pdf = sample_pdf(cur_z); - double area = 0.5 * (pdf_vector[i - 1] + cur_pdf) * (zspline[i] - zspline[i - 1]); - pdf_vector.push_back(cur_pdf); - cdf_z_nodes.push_back(cur_z); - cdf_vector.push_back(area + cdf_vector.back()); - } - - cdf_z_nodes.push_back(1); - cdf_vector.push_back(1); - pdf_vector.push_back(0); - - - // set the spline table - siren::utilities::TableData1D inverse_cdf_data; - inverse_cdf_data.x = cdf_vector; - inverse_cdf_data.f = cdf_z_nodes; - - inverseCdfTable = siren::utilities::Interpolator1D(inverse_cdf_data); - - return; -} - -double QuarkDISFromSpline::sample_pdf(double x) const { - return (0.8 / x ) / (std::pow(1 - (1 / x) - (0.2 / (1 - x)), 2)) / fragmentation_integral; -} - -double QuarkDISFromSpline::TotalCrossSection(dataclasses::InteractionRecord const & interaction) const { - siren::dataclasses::ParticleType primary_type = interaction.signature.primary_type; - rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); - double primary_energy; - primary_energy = interaction.primary_momentum[0]; - // if we are below threshold, return 0 - if(primary_energy < InteractionThreshold(interaction)) { - std::cout << "DIS::interaction threshold not satisfied" << std::endl; - return 0; - } - return TotalCrossSection(primary_type, primary_energy); -} - -double QuarkDISFromSpline::TotalCrossSection(siren::dataclasses::ParticleType primary_type, double primary_energy) const { - if(not primary_types_.count(primary_type)) { - throw std::runtime_error("Supplied primary not supported by cross section!"); - } - double log_energy = log10(primary_energy); - - if(log_energy < total_cross_section_.lower_extent(0) - or log_energy > total_cross_section_.upper_extent(0)) { - throw std::runtime_error("Interaction energy ("+ std::to_string(primary_energy) + - ") out of cross section table range: [" - + std::to_string(pow(10.,total_cross_section_.lower_extent(0))) + " GeV," - + std::to_string(pow(10.,total_cross_section_.upper_extent(0))) + " GeV]"); - } - - int center; - total_cross_section_.searchcenters(&log_energy, ¢er); - - double log_xs = total_cross_section_.ndsplineeval(&log_energy, ¢er, 0); - if (std::pow(10.0, log_xs) == 0) { - std::cout << "DIS::cross section evaluated to 0" << std::endl; - } - - return unit * std::pow(10.0, log_xs); -} - -double QuarkDISFromSpline::DifferentialCrossSection(dataclasses::InteractionRecord const & interaction) const { - rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); - rk::P4 p2(geom3::Vector3(0, 0, 0), interaction.target_mass); - double primary_energy; - primary_energy = interaction.primary_momentum[0]; - assert(interaction.signature.secondary_types.size() == 3); - std::map secondaries = getIndices(interaction.signature); - unsigned int lepton_index = secondaries["lepton"]; - unsigned int hadron_index = secondaries["hadron"]; - unsigned int meson_index = secondaries["meson"]; - - std::array const & mom3 = interaction.secondary_momenta[lepton_index]; - std::array const & mom_x = interaction.secondary_momenta[hadron_index]; - std::array const & mom_d = interaction.secondary_momenta[meson_index]; - - rk::P4 p3(geom3::Vector3(mom3[1], mom3[2], mom3[3]), interaction.secondary_masses[lepton_index]); - rk::P4 p_x(geom3::Vector3(mom_x[1], mom_x[2], mom_x[3]), interaction.secondary_masses[hadron_index]); - rk::P4 p_d(geom3::Vector3(mom_d[1], mom_d[2], mom_d[3]), interaction.secondary_masses[meson_index]); - rk::P4 p4 = p_x + p_d; // this assume that we are working in a good frame where the hadronization vertex has 4-momentum conserved - rk::P4 q = p1 - p3; - // however p4 is not used in computation here so we should be fine... - - double Q2 = -q.dot(q); - double x, y; - double lepton_mass = GetLeptonMass(interaction.signature.secondary_types[lepton_index]); - - y = 1.0 - p2.dot(p3) / p2.dot(p1); - x = Q2 / (2.0 * p2.dot(q)); - double log_energy = log10(primary_energy); - std::array coordinates{{log_energy, log10(x), log10(y)}}; - std::array centers; - - if (Q2 < minimum_Q2_ || !kinematicallyAllowed(x, y, primary_energy, target_mass_, lepton_mass) - || !differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { - // std::cout << "weighting: revert back to saved x and y" << std::endl; - double E1_lab = interaction.interaction_parameters.at("energy"); - double E2_lab = p2.e(); - x = interaction.interaction_parameters.at("bjorken_x"); - y = interaction.interaction_parameters.at("bjorken_y"); - Q2 = 2. * E1_lab * E2_lab * x * y; - } - return DifferentialCrossSection(primary_energy, x, y, lepton_mass, Q2); -} - -double QuarkDISFromSpline::DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2) const { - double log_energy = log10(energy); - // check preconditions - if(log_energy < differential_cross_section_.lower_extent(0) - || log_energy>differential_cross_section_.upper_extent(0)) - {std::cout << "Diff xsec: not in bounds" << std::endl; - return 0.0;} - if(x <= 0 || x >= 1) { - std::cout << "x is out of bounds with x = " << x << std::endl; - return 0.0; - } - if(y <= 0 || y >= 1){ - std::cout << "y is out of bounds with x = " << y << std::endl; - return 0.0; - } - - if(std::isnan(Q2)) { - Q2 = 2.0 * energy * target_mass_ * x * y; - } - if(Q2 < minimum_Q2_) { - std::cout << "Q2 is smaller than minimum Q2 with " << Q2 << " < " << minimum_Q2_ << std::endl; - return 0; - } // cross section not calculated, assumed to be zero - - if(!kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) { - std::cout << "not kinematically allowed!" << std::endl; - return 0; - } - std::array coordinates{{log_energy, log10(x), log10(y)}}; - std::array centers; - if(!differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { - std::cout << "search centers failed!" << std::endl; - return 0; - } - double result = pow(10., differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0)); - assert(result >= 0); - if (std::isinf(result)) { - std::cout << "energy, x, y, Q2 are " << energy << " " << x << " " << y << " " << Q2 << " " << std::endl; - std::cout << "spline value read is " << differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0) << std::endl; - } - return unit * result; -} - -double QuarkDISFromSpline::InteractionThreshold(dataclasses::InteractionRecord const & interaction) const { - // Consider implementing DIS thershold at some point - return 0; -} - -void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { - // first obtain the indices from secondaries - // std::cout << "in sample final state" << std::endl; - std::map secondary_indices = getIndices(record.signature); - unsigned int lepton_index = secondary_indices["lepton"]; - unsigned int hadron_index = secondary_indices["hadron"]; - unsigned int meson_index = secondary_indices["meson"]; - - // Uses Metropolis-Hastings Algorithm! - // useful for cases where we don't know the supremum of our distribution, and the distribution is multi-dimensional - if (differential_cross_section_.get_ndim() != 3) { - throw std::runtime_error("I expected 3 dimensions in the cross section spline, but got " + std::to_string(differential_cross_section_.get_ndim()) +". Maybe your fits file doesn't have the right 'INTERACTION' key?"); - } - rk::P4 p1(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); - // std::cout << "quark::sampleFinalState : primary momentum is read to be " << p1 << std::endl; - rk::P4 p2(geom3::Vector3(0, 0, 0), target_mass_); - - // we assume that: - // the target is stationary so its energy is just its mass - // the incoming neutrino is massless, so its kinetic energy is its total energy - // double s = target_mass_ * trecord.secondary_momentarget_mass_ + 2 * target_mass_ * primary_energy; - // double s = std::pow(rk::invMass(p1, p2), 2); - - double primary_energy; - rk::P4 p1_lab; - rk::P4 p2_lab; - p1_lab = p1; - p2_lab = p2; - primary_energy = p1_lab.e(); - - // correctly assign lepton, hadron and meson index - double m = GetLeptonMass(record.signature.secondary_types[lepton_index]); - - double m1 = record.primary_mass; - double m3 = m; - double E1_lab = p1_lab.e(); - double E2_lab = p2_lab.e(); - - // The out-going particle always gets at least enough energy for its rest mass - double yMax = 1 - m / primary_energy; - double logYMax = log10(yMax); - - // The minimum allowed value of y occurs when x = 1 and Q is minimized - double yMin = minimum_Q2_ / (2 * E1_lab * E2_lab); - double logYMin = log10(yMin); - // The minimum allowed value of x occurs when y = yMax and Q is minimized - // double xMin = minimum_Q2_ / ((s - target_mass_ * target_mass_) * yMax); - double xMin = minimum_Q2_ / (2 * E1_lab * E2_lab * yMax); - double logXMin = log10(xMin); - - bool accept; - - // kin_vars and its twin are 3-vectors containing [nu-energy, Bjorken X, Bjorken Y] - std::array kin_vars, test_kin_vars; - - // centers of the cross section spline tales. - std::array spline_table_center, test_spline_table_center; - - // values of cross_section from the splines. By * Bx * Spline(E,x,y) - double cross_section, test_cross_section; - - // No matter what, we're evaluating at this specific energy. - kin_vars[0] = test_kin_vars[0] = log10(primary_energy); - - // check preconditions - if(kin_vars[0] < differential_cross_section_.lower_extent(0) - || kin_vars[0] > differential_cross_section_.upper_extent(0)) - throw std::runtime_error("Interaction energy out of cross section table range: [" - + std::to_string(pow(10.,differential_cross_section_.lower_extent(0))) + " GeV," - + std::to_string(pow(10.,differential_cross_section_.upper_extent(0))) + " GeV]"); - - // sample an intial point - do { - // rejection sample a point which is kinematically allowed by calculation limits - double trialQ; - double trials = 0; - do { - if (trials >= 100) throw std::runtime_error("too much trials"); - trials += 1; - kin_vars[1] = random->Uniform(logXMin,0); - kin_vars[2] = random->Uniform(logYMin,logYMax); - trialQ = (2 * E1_lab * E2_lab) * pow(10., kin_vars[1] + kin_vars[2]); - } while(trialQ differential_cross_section_.upper_extent(1)) { - accept = false; - } - if(kin_vars[2] < differential_cross_section_.lower_extent(2) - || kin_vars[2] > differential_cross_section_.upper_extent(2)) { - accept = false; - } - - if(accept) { - // finds the centers in the cross section spline table, returns true if it's successful - // also sets the centers - accept = differential_cross_section_.searchcenters(kin_vars.data(),spline_table_center.data()); - } - } while(!accept); - - //TODO: better proposal distribution? - double measure = pow(10., kin_vars[1] + kin_vars[2]); // Bx * By - - // Bx * By * xs(E, x, y) - // evalutates the differential spline at that point - cross_section = measure*pow(10., differential_cross_section_.ndsplineeval(kin_vars.data(), spline_table_center.data(), 0)); - - // this is the magic part. Metropolis Hastings Algorithm. - // MCMC method! - const size_t burnin = 40; // converges to the correct distribution over multiple samplings. - // big number means more accurate, but slower - for(size_t j = 0; j <= burnin; j++) { - // repeat the sampling from above to get a new valid point - double trialQ; - do { - test_kin_vars[1] = random->Uniform(logXMin, 0); - test_kin_vars[2] = random->Uniform(logYMin, logYMax); - trialQ = (2 * E1_lab * E2_lab) * pow(10., test_kin_vars[1] + test_kin_vars[2]); - } while(trialQ < minimum_Q2_ || !kinematicallyAllowed(pow(10., test_kin_vars[1]), pow(10., test_kin_vars[2]), primary_energy, target_mass_, m)); - - accept = true; - if(test_kin_vars[1] < differential_cross_section_.lower_extent(1) - || test_kin_vars[1] > differential_cross_section_.upper_extent(1)) - accept = false; - if(test_kin_vars[2] < differential_cross_section_.lower_extent(2) - || test_kin_vars[2] > differential_cross_section_.upper_extent(2)) - accept = false; - if(!accept) - continue; - - accept = differential_cross_section_.searchcenters(test_kin_vars.data(), test_spline_table_center.data()); - if(!accept) - continue; - - double measure = pow(10., test_kin_vars[1] + test_kin_vars[2]); - double eval = differential_cross_section_.ndsplineeval(test_kin_vars.data(), test_spline_table_center.data(), 0); - if(std::isnan(eval)) - continue; - test_cross_section = measure * pow(10., eval); - - double odds = (test_cross_section / cross_section); - accept = (cross_section == 0 || (odds > 1.) || random->Uniform(0, 1) < odds); - - if(accept) { - kin_vars = test_kin_vars; - cross_section = test_cross_section; - // std::cout << "trial Q is" << trialQ << std::endl; - } - } - - // scaling down to handle numerical issues - double final_x = pow(10., kin_vars[1]); - double final_y = pow(10., kin_vars[2]); - record.interaction_parameters.clear(); - record.interaction_parameters["energy"] = E1_lab; - record.interaction_parameters["bjorken_x"] = final_x; - record.interaction_parameters["bjorken_y"] = final_y; - - double Q2 = 2 * E1_lab * E2_lab * pow(10.0, kin_vars[1] + kin_vars[2]); - double p1x_lab = std::sqrt(p1_lab.px() * p1_lab.px() + p1_lab.py() * p1_lab.py() + p1_lab.pz() * p1_lab.pz()); - double pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); - double momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); - double pqy_lab, Eq_lab; - - if (pqx_lab>momq_lab){ - // if current setting does not work, start looping through scalings - int maxIterations = 10; - int iteration = 0; - double p1_lab_x = p1_lab.px(); - double p1_lab_y = p1_lab.py(); - double p1_lab_z = p1_lab.pz(); - // loop to resolve precision issue - while (iteration <= maxIterations) { - Q2 = 2. * E1_lab * E2_lab * pow(10.0, kin_vars[1] + kin_vars[2]); - p1x_lab = std::sqrt(p1_lab_x * p1_lab_x + p1_lab_y * p1_lab_y + p1_lab_z * p1_lab_z); - pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); - momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); - if (pqx_lab>momq_lab){ - // std::cout << "triggered on " << momq_lab << " and " << pqx_lab << std::endl; - //scale down - E1_lab /= 10; - E2_lab /= 10; - p1_lab_x /= 10; - p1_lab_y /= 10; - p1_lab_z /= 10; - m1 /= 10; - m3 /= 10; - //iteration += 1 to scale back - iteration += 1; - continue; - } - pqy_lab = std::sqrt((momq_lab + pqx_lab) * (momq_lab - pqx_lab)); - // std::cout << "finished with " << iteration << " iterations and " << momq_lab << " and " << pqx_lab << std::endl; - break; - } - // //scale back - if (iteration > 0) { - // std::cout << "scaling back with " << pow(10.0, iteration); - E1_lab *= pow(10.0, iteration); - E2_lab *= pow(10.0, iteration); - p1_lab_x *= pow(10.0, iteration); - p1_lab_y *= pow(10.0, iteration); - p1_lab_z *= pow(10.0, iteration); - m1 *= pow(10.0, iteration); - m3 *= pow(10.0, iteration); - // std::cout << "and finished with " << momq_lab << " and " << pqx_lab << std::endl; - } - // pqy_lab = 0; - } else {pqy_lab = std::sqrt(momq_lab*momq_lab - pqx_lab *pqx_lab);} - Eq_lab = E1_lab * final_y; - - geom3::UnitVector3 x_dir = geom3::UnitVector3::xAxis(); - geom3::Vector3 p1_mom = p1_lab.momentum(); - geom3::UnitVector3 p1_lab_dir = p1_mom.direction(); - geom3::Rotation3 x_to_p1_lab_rot = geom3::rotationBetween(x_dir, p1_lab_dir); - - double phi = random->Uniform(0, 2.0 * M_PI); - geom3::Rotation3 rand_rot(p1_lab_dir, phi); - - rk::P4 pq_lab(Eq_lab, geom3::Vector3(pqx_lab, pqy_lab, 0)); - pq_lab.rotate(x_to_p1_lab_rot); - pq_lab.rotate(rand_rot); - - rk::P4 p3_lab((p1_lab - pq_lab).momentum(), m3); - rk::P4 p4_lab = p2_lab + pq_lab; - - rk::P4 p3; - rk::P4 p4; - p3 = p3_lab; // now we have our lepton momentum set, which should not be modified from here on - p4 = p4_lab; // momentum of the virtual charm - // std::cout << "charm momentum is " << p4 << std::endl; - - // compute the energy and 3-momentum of the virtual charm - // std::cout << "the virtual charm off-shell mass is " << p4.m() << std::endl; - double p3c = std::sqrt(std::pow(p4.px(), 2) + std::pow(p4.py(), 2) + std::pow(p4.pz(), 2)); - double Ec = p4.e(); //energy of primary charm - double mCH = getHadronMass(record.signature.secondary_types[meson_index]); // obtain charmed hadron mass - - // accept-reject sampling for a valid momentum fragmentation - bool frag_accept; - double randValue; - double z; - double ECH; - - // add a maximum number of trials in the while loop - int max_sampling = 500; - int sampling = 0; - - // sample again if this eenrgy is not kinematically allowed - // this samples in the lab frame the energy of the D-meson such that mass is real - do { - sampling += 1; - if (sampling > max_sampling) { - std::cout << "energy of the charm is " << Ec << " and momentum is " << p3c << std::endl; - std::cout << "desired mass of hadron is " << mCH << std::endl; - // throw(siren::utilities::InjectionFailure("Failed to sample hadronization!")); - break; - } - randValue = random->Uniform(0,1); - z = inverseCdfTable(randValue); - ECH = z * Ec; - if (std::pow(ECH, 2) - std::pow(mCH, 2) <= 0) { - frag_accept = false; - } else { - frag_accept = true; - } - } while (!frag_accept); - // new attempt of using the isoscalar mass as the remnant hadronic shower mass - double mX = target_mass_; - double Mc = p4.m(); - // std::cout << "using remnant mass " << mX << std::endl; - // std::cout << "invariant charm mass and its energy is " << Mc << ", " << p4.e() << std::endl; - // std::cout << "target sampled D meson energy is " << ECH << std::endl; - // std::cout << "and the fraction of momentum is sampled to be " << z << std::endl; - //compute the energies in the charm rest frame - double E_CH_c = (std::pow(Mc, 2) - std::pow(mX, 2) + std::pow(mCH, 2)) / (2 * Mc); - // std::cout << "energy of charm in rest frame is " << E_CH_c << std::endl; - double p_c = std::sqrt((std::pow(Mc, 2) - std::pow(mCH + mX, 2)) * (std::pow(Mc, 2) - std::pow(mCH - mX, 2))) / (2 * Mc); - // std::cout << "momentum in charm rest frame is " << p_c << std::endl; - // compute the lorentz boost parameters - double gamma = p4.gamma(); - double beta = p4.beta(); - // std::cout << "beta and gamma parameters are " << beta << ", " << gamma << std::endl; - // using the lab frame fragmented energy and the - double cosTheta = std::max(std::min(((ECH - gamma * E_CH_c)/(gamma * beta * p_c)), 1.), -1.); - // std::cout << "cosine of theta in charm frame is " << cosTheta << std::endl; - // std::cout << "without cutting, the number is " << (ECH - gamma * E_CH_c)/(gamma * beta * p_c) << std::endl; - // now compute the momentum vectors in the rest frame - double sinTheta = std::sin(std::acos(cosTheta)); - // std::cout << "and sine of theta is computed to be " << sinTheta << std::endl; - rk::P4 p4CH_c(p_c * geom3::Vector3(cosTheta, sinTheta, 0), mCH); - rk::P4 p4X_c(p_c * geom3::Vector3(-cosTheta, -sinTheta, 0), mX); - // these all assume boost direction is charm direction. Now we should rotate back to charm lab momentum direction - geom3::Vector3 pc_lab_momentum = p4.momentum(); - geom3::UnitVector3 pc_lab_dir = pc_lab_momentum.direction(); - geom3::Rotation3 x_to_pc_lab_rot = geom3::rotationBetween(x_dir, pc_lab_dir); - p4X_c.rotate(x_to_pc_lab_rot); - p4CH_c.rotate(x_to_pc_lab_rot); - - // finally, we perform a random azimuthal rotation - double c_phi = random->Uniform(0, 2 * M_PI); - geom3::Rotation3 azimuth_rand_rot(pc_lab_dir, c_phi); - p4X_c.rotate(azimuth_rand_rot); - p4CH_c.rotate(azimuth_rand_rot); - - // and boost them back to the lab frame - rk::Boost boost_from_crest_to_lab = p4.labBoost(); - rk::P4 p4X = p4X_c.boost(boost_from_crest_to_lab); - rk::P4 p4CH = p4CH_c.boost(boost_from_crest_to_lab); - - // std::cout << "computed remnant mass and energy is " << p4X.m() << ", " << p4X.e() << std::endl; - // std::cout << "and computed D mass and energy is " << p4CH.m() << ", " << p4CH.e() << std::endl; - // std::cout << "target sampled D meson energy is " << ECH << std::endl; - - - // now we proceed to saving the final state kinematics - std::vector & secondaries = record.GetSecondaryParticleRecords(); - siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[lepton_index]; - siren::dataclasses::SecondaryParticleRecord & hadron = secondaries[hadron_index]; - siren::dataclasses::SecondaryParticleRecord & meson = secondaries[meson_index]; - // std::cout << "QuarkDIS::SampleFInalState : the indices are: " << lepton_index << hadron_index<< meson_index << std::endl; - - lepton.SetFourMomentum({p3.e(), p3.px(), p3.py(), p3.pz()}); - // std::cout << "setting lepton mass with lepton momentum " << p3 << std::endl; - lepton.SetMass(p3.m()); - lepton.SetHelicity(record.primary_helicity); - hadron.SetFourMomentum({p4X.e(), p4X.px(), p4X.py(), p4X.pz()}); - // std::cout << "setting hadron mass with hadron momentum " << p4X << std::endl; - hadron.SetMass(p4X.m()); - hadron.SetHelicity(record.target_helicity); - meson.SetFourMomentum({p4CH.e(), p4CH.px(), p4CH.py(), p4CH.pz()}); - // std::cout << "setting meson mass with meson momentum " << p4CH << std::endl; - meson.SetMass(p4CH.m()); - meson.SetHelicity(record.target_helicity); // this needs working on - // std::cout << "finished sampling final state" << std::endl; -} - -double QuarkDISFromSpline::FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const { - if (secondary == siren::dataclasses::Particle::ParticleType::D0 || secondary == siren::dataclasses::Particle::ParticleType::D0Bar) { - return 0.6; - } else if (secondary == siren::dataclasses::Particle::ParticleType::DPlus || secondary == siren::dataclasses::Particle::ParticleType::DMinus) { - return 0.23; - } // D_s and Lambda^+ not yet implemented - return 0; -} - -double QuarkDISFromSpline::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { - // first compute the differential and total cross section - double dxs = DifferentialCrossSection(interaction); - // if (dxs == 0) { - // std::cout << "diff xsec gives 0" << std::endl; - // } - double txs = TotalCrossSection(interaction); - //then compute the fragmentation probability - std::map secondaries = getIndices(interaction.signature); - unsigned int meson_index = secondaries["meson"]; - double fragfrac = FragmentationFraction(interaction.signature.secondary_types[meson_index]); - if(dxs == 0) { - std::cout << "diff xsec gives 0" << std::endl; - return 0.0; - } else { - // if (txs == 0) {std::cout << "wtf??? txs is 0 in final state prob" << txs << std::endl;} - if (std::isinf(dxs)) {std::cout << "dxs is inf in final state prob" << std::endl;} - return dxs / txs * fragfrac; - } -} - -std::vector QuarkDISFromSpline::GetPossiblePrimaries() const { - return std::vector(primary_types_.begin(), primary_types_.end()); -} - -std::vector QuarkDISFromSpline::GetPossibleTargetsFromPrimary(siren::dataclasses::ParticleType primary_type) const { - return std::vector(target_types_.begin(), target_types_.end()); -} - -std::vector QuarkDISFromSpline::GetPossibleSignatures() const { - return std::vector(signatures_.begin(), signatures_.end()); -} - -std::vector QuarkDISFromSpline::GetPossibleTargets() const { - return std::vector(target_types_.begin(), target_types_.end()); -} - -std::vector QuarkDISFromSpline::GetPossibleSignaturesFromParents(siren::dataclasses::ParticleType primary_type, siren::dataclasses::ParticleType target_type) const { - std::pair key(primary_type, target_type); - if(signatures_by_parent_types_.find(key) != signatures_by_parent_types_.end()) { - return signatures_by_parent_types_.at(key); - } else { - return std::vector(); - } -} - -std::vector QuarkDISFromSpline::DensityVariables() const { - return std::vector{"Bjorken x", "Bjorken y"}; -} - -} // namespace interactions -} // namespace siren From d94184ca96767c3097bb581d41fd39761ce100ef Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Wed, 25 Mar 2026 17:22:31 -0400 Subject: [PATCH 17/93] fix typos in charm decay: previous Q^2 distribution is incorrect, but the kinematics should be energy and momentum conserving, so the Q^2 distortion would be the only effect --- projects/interactions/private/CharmMesonDecay.cxx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx index f70752b16..3508ce9cb 100644 --- a/projects/interactions/private/CharmMesonDecay.cxx +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -49,7 +49,7 @@ CharmMesonDecay::CharmMesonDecay(siren::dataclasses::Particle::ParticleType prim if (primary == siren::dataclasses::Particle::ParticleType::DPlus) { constants[0] = 0.725; // this is f^+(0)|V_cs| for charged D - constants[1] = 0.50; // this is alpha, same for all K final states + constants[1] = 0.44; // this is alpha, same for all K final states constants[2] = 2.01027; // this is excited charged D meson mD = particleMass(siren::dataclasses::Particle::ParticleType::DPlus); @@ -267,8 +267,8 @@ double CharmMesonDecay::DifferentialDecayWidth(std::vector constants, do double ms = constants[2]; double Q2tilde = Q2 / ms; // compute the 3-momentum as a function of Q2 - double EK = 0.5 * (Q2 - pow(mD, 2 + pow(mK, 2))) / mD; // energy of Kaon - double PK = pow(pow(EK, 2) - pow(mK, 2), 1/2); + double EK = 0.5 * (Q2 - pow(mD, 2) + pow(mK, 2)) / mD; // energy of Kaon + double PK = pow(pow(EK, 2) - pow(mK, 2), 0.5); // plug in the constants double dGamma = pow(siren::utilities::Constants::FermiConstant,2) / (24 * pow(siren::utilities::Constants::pi,3)) * pow(F0CKM,2) * pow((1/((1-Q2tilde) * (1 - alpha * Q2tilde))),2) * pow(PK,3); From 206c13d20eac1a7ca48f056a3a642b32fa71b982 Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Thu, 2 Apr 2026 15:17:52 -0400 Subject: [PATCH 18/93] fixing CDF sampling in charm meson decay --- projects/interactions/CMakeLists.txt | 1 + .../interactions/private/CharmMesonDecay.cxx | 6 +- .../private/test/CharmMesonDecay_TEST.cxx | 356 ++++++++++++++++++ 3 files changed, 361 insertions(+), 2 deletions(-) create mode 100644 projects/interactions/private/test/CharmMesonDecay_TEST.cxx diff --git a/projects/interactions/CMakeLists.txt b/projects/interactions/CMakeLists.txt index ddd9936c8..203816009 100644 --- a/projects/interactions/CMakeLists.txt +++ b/projects/interactions/CMakeLists.txt @@ -65,3 +65,4 @@ set_target_properties(interactions PROPERTIES BUILD_WITH_INSTALL_RPATH FALSE LINK_FLAGS "-Wl,-rpath,\\\$ORIGIN/../siren.libs/") endif() +package_add_test(UnitTest_CharmMesonDecay ${PROJECT_SOURCE_DIR}/projects/interactions/private/test/CharmMesonDecay_TEST.cxx) diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx index 3508ce9cb..3a8a6641e 100644 --- a/projects/interactions/private/CharmMesonDecay.cxx +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -265,9 +265,11 @@ double CharmMesonDecay::DifferentialDecayWidth(std::vector constants, do double F0CKM = constants[0]; double alpha = constants[1]; double ms = constants[2]; - double Q2tilde = Q2 / ms; + double Q2tilde = Q2 / (ms * ms); // compute the 3-momentum as a function of Q2 - double EK = 0.5 * (Q2 - pow(mD, 2) + pow(mK, 2)) / mD; // energy of Kaon + // double EK = 0.5 * (Q2 - pow(mD, 2) + pow(mK, 2)) / mD; // energy of Kaon + double EK = 0.5 * (Q2 - (pow(mD, 2) + pow(mK, 2))) / mD; // energy of Kaon + double PK = pow(pow(EK, 2) - pow(mK, 2), 0.5); // plug in the constants double dGamma = pow(siren::utilities::Constants::FermiConstant,2) / (24 * pow(siren::utilities::Constants::pi,3)) * pow(F0CKM,2) * diff --git a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx new file mode 100644 index 000000000..682b97212 --- /dev/null +++ b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx @@ -0,0 +1,356 @@ +/** + * Unit test for CharmMesonDecay CDF and Interpolator1D behavior. + * + * Tests: + * 1. Interpolator1D with a known linear inverse CDF (sanity check) + * 2. Interpolator1D with the actual D meson decay CDF table + * 3. CharmMesonDecay::SampleFinalState Q² distribution + * + * Build (from SIREN/build): + * make -j4 (if registered in CMakeLists.txt) + * Or standalone: + * g++ -std=c++17 -I../projects/utilities/public -I../projects/interactions/public \ + * -I../projects/dataclasses/public -I../vendor/cereal/include \ + * -I../vendor/rk/include -I../vendor/photospline/include \ + * -L. -lSIREN -o CharmMesonDecay_TEST CharmMesonDecay_TEST.cxx -lgtest -lgtest_main + */ +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "SIREN/utilities/Interpolator.h" +#include "SIREN/utilities/Integration.h" +#include "SIREN/utilities/Constants.h" +#include "SIREN/utilities/Random.h" +#include "SIREN/interactions/CharmMesonDecay.h" +#include "SIREN/dataclasses/Particle.h" +#include "SIREN/dataclasses/InteractionRecord.h" +#include "SIREN/dataclasses/InteractionSignature.h" + +using namespace siren::utilities; +using namespace siren::interactions; +using namespace siren::dataclasses; + +// ── Test 1: Interpolator1D with known linear CDF ───────────────────────── + +TEST(Interpolator1D, LinearInverseCDF) { + // CDF: F(x) = x / 1.4, so inverse: x = 1.4 * u + // Table: x = CDF values, f = Q2 values + TableData1D table; + int N = 102; + for (int i = 0; i < N; ++i) { + double u = (double)i / (N - 1); // CDF: 0 to 1 + double q2 = u * 1.4; // Q2: 0 to 1.4 + table.x.push_back(u); + table.f.push_back(q2); + } + + Interpolator1D interp(table); + + std::cout << "Test 1: Linear inverse CDF" << std::endl; + std::cout << " IsLog: " << interp.IsLog() << std::endl; + + // Check known values + double tol = 0.01; + EXPECT_NEAR(interp(0.0), 0.0, tol); + EXPECT_NEAR(interp(0.25), 0.35, tol); + EXPECT_NEAR(interp(0.5), 0.7, tol); + EXPECT_NEAR(interp(0.75), 1.05, tol); + EXPECT_NEAR(interp(1.0), 1.4, tol); + + std::cout << " interp(0.0) = " << interp(0.0) << " (expect 0.0)" << std::endl; + std::cout << " interp(0.25) = " << interp(0.25) << " (expect 0.35)" << std::endl; + std::cout << " interp(0.5) = " << interp(0.5) << " (expect 0.7)" << std::endl; + std::cout << " interp(0.75) = " << interp(0.75) << " (expect 1.05)" << std::endl; + std::cout << " interp(1.0) = " << interp(1.0) << " (expect 1.4)" << std::endl; + + // Sample mean should be 0.7 + double sum = 0; + int Nsamp = 10000; + SIREN_random rng; + for (int i = 0; i < Nsamp; ++i) { + double u = rng.Uniform(0, 1); + sum += interp(u); + } + double mean = sum / Nsamp; + std::cout << " Mean from " << Nsamp << " samples: " << mean << " (expect ~0.7)" << std::endl; + EXPECT_NEAR(mean, 0.7, 0.05); +} + +// ── Test 2: Interpolator1D with D meson decay CDF ─────────────────────── + +TEST(Interpolator1D, DMesonDecayCDF) { + // Replicate computeDiffGammaCDF for D0 -> K- e+ nu_e + double mD = Constants::D0Mass; + double mK = Constants::KminusMass; + double F0CKM = 0.719; + double alpha = 0.50; + double ms = 2.00697; + double GF = Constants::FermiConstant; + + std::cout << "\nTest 2: D meson decay CDF" << std::endl; + std::cout << " mD = " << mD << ", mK = " << mK << std::endl; + + // DifferentialDecayWidth (fixed version) + auto dGamma = [&](double Q2) -> double { + double Q2tilde = Q2 / (ms * ms); + double ff2 = std::pow(F0CKM / ((1 - Q2tilde) * (1 - alpha * Q2tilde)), 2); + double EK = 0.5 * (Q2 - (mD * mD + mK * mK)) / mD; + double pk_sq = EK * EK - mK * mK; + if (pk_sq < 0) return 0.0; + double PK = std::sqrt(pk_sq); + return std::pow(GF, 2) / (24 * std::pow(M_PI, 3)) * ff2 * std::pow(PK, 3); + }; + + // Normalize (same as SIREN: Romberg integration over [0, 1.4]) + std::function pdf_func = dGamma; + double norm = rombergIntegrate(pdf_func, 0.0, 1.4); + std::cout << " Normalization: " << norm << std::endl; + + auto normed_pdf = [&](double Q2) -> double { + return dGamma(Q2) / norm; + }; + + // Build CDF table (same as SIREN: 100 nodes from 0.01 to ~1.39) + double Q2_min = 0.0; + double Q2_max = 1.4; + std::vector Q2spline; + for (int i = 0; i < 100; ++i) { + Q2spline.push_back(0.01 + i * (Q2_max - Q2_min) / 100); + } + + std::vector cdf_Q2_nodes; + std::vector cdf_vector; + std::vector pdf_vector; + + cdf_Q2_nodes.push_back(0); + cdf_vector.push_back(0); + pdf_vector.push_back(0); + + for (size_t i = 0; i < Q2spline.size(); ++i) { + double cur_Q2 = Q2spline[i]; + double cur_pdf = normed_pdf(cur_Q2); + double area; + if (i == 0) { + area = cur_Q2 * cur_pdf * 0.5; + } else { + area = 0.5 * (pdf_vector[i - 1] + cur_pdf) * (Q2spline[i] - Q2spline[i - 1]); + } + pdf_vector.push_back(cur_pdf); + cdf_Q2_nodes.push_back(cur_Q2); + cdf_vector.push_back(area + cdf_vector.back()); + } + + cdf_Q2_nodes.push_back(Q2_max); + cdf_vector.push_back(1.0); + pdf_vector.push_back(0); + + std::cout << " CDF table size: " << cdf_vector.size() << " nodes" << std::endl; + std::cout << " CDF last value before forced 1.0: " << cdf_vector[cdf_vector.size() - 2] << std::endl; + + // Build Interpolator1D (inverse CDF: x=CDF, f=Q2) + TableData1D inverse_cdf_data; + inverse_cdf_data.x = cdf_vector; + inverse_cdf_data.f = cdf_Q2_nodes; + + Interpolator1D inverseCdf(inverse_cdf_data); + + std::cout << " Interpolator1D IsLog: " << inverseCdf.IsLog() << std::endl; + std::cout << " MinX: " << inverseCdf.MinX() << ", MaxX: " << inverseCdf.MaxX() << std::endl; + + // Evaluate at known CDF values + std::cout << "\n Inverse CDF spot checks:" << std::endl; + double test_cdfs[] = {0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}; + for (double u : test_cdfs) { + double q2 = inverseCdf(u); + // Also compute expected by linear interpolation + double q2_expected = 0; + for (size_t j = 0; j < cdf_vector.size() - 1; ++j) { + if (cdf_vector[j] <= u && u <= cdf_vector[j + 1]) { + double t = (cdf_vector[j + 1] - cdf_vector[j] > 0) ? + (u - cdf_vector[j]) / (cdf_vector[j + 1] - cdf_vector[j]) : 0; + q2_expected = cdf_Q2_nodes[j] + (cdf_Q2_nodes[j + 1] - cdf_Q2_nodes[j]) * t; + break; + } + } + std::cout << " CDF=" << u << " -> Q2=" << q2 << " (linear expected: " << q2_expected + << ", diff=" << q2 - q2_expected << ")" << std::endl; + } + + // Sample and compute mean Q² + SIREN_random rng; + int Nsamp = 100000; + double sum_q2 = 0; + double sum_q2_linear = 0; + for (int i = 0; i < Nsamp; ++i) { + double u = rng.Uniform(0, 1); + sum_q2 += inverseCdf(u); + // Linear interpolation for comparison + double q2_lin = 0; + for (size_t j = 0; j < cdf_vector.size() - 1; ++j) { + if (cdf_vector[j] <= u && u <= cdf_vector[j + 1]) { + double t = (cdf_vector[j + 1] - cdf_vector[j] > 0) ? + (u - cdf_vector[j]) / (cdf_vector[j + 1] - cdf_vector[j]) : 0; + q2_lin = cdf_Q2_nodes[j] + (cdf_Q2_nodes[j + 1] - cdf_Q2_nodes[j]) * t; + break; + } + } + sum_q2_linear += q2_lin; + } + double mean_interp = sum_q2 / Nsamp; + double mean_linear = sum_q2_linear / Nsamp; + + std::cout << "\n Mean Q² from Interpolator1D: " << mean_interp << std::endl; + std::cout << " Mean Q² from linear interp: " << mean_linear << std::endl; + std::cout << " Expected (correct): ~0.58" << std::endl; + std::cout << " Difference: " << mean_interp - mean_linear << std::endl; + + // The test: Interpolator1D should give similar results to linear interpolation + // If not, this confirms the Interpolator1D bias + if (std::abs(mean_interp - mean_linear) > 0.05) { + std::cout << "\n *** INTERPOLATOR1D BIAS DETECTED ***" << std::endl; + std::cout << " Interpolator1D gives " << mean_interp << " vs linear " << mean_linear << std::endl; + } + EXPECT_NEAR(mean_interp, mean_linear, 0.05); +} + +// ── Test 2b: Compare DifferentialDecayWidth with analytical formula ────── + +TEST(CharmMesonDecay, DiffDecayWidthComparison) { + // Check if the SIREN DifferentialDecayWidth matches our analytical formula + double mD = Constants::D0Mass; // 1.86962 (note: swapped with DPlus in Constants.h!) + double mK = Constants::KminusMass; + double F0CKM = 0.719; + double alpha = 0.50; + double ms_star = 2.00697; + double GF = Constants::FermiConstant; + + std::cout << "\nTest 2b: DifferentialDecayWidth comparison" << std::endl; + std::cout << " Constants::D0Mass = " << Constants::D0Mass << " (PDG D0 = 1.86484, PDG D+ = 1.86966)" << std::endl; + std::cout << " Constants::DPlusMass = " << Constants::DPlusMass << " (SWAPPED!)" << std::endl; + + // Our analytical formula + auto dGamma_analytical = [&](double Q2) -> double { + double Q2tilde = Q2 / (ms_star * ms_star); + double ff2 = std::pow(F0CKM / ((1 - Q2tilde) * (1 - alpha * Q2tilde)), 2); + double EK = 0.5 * (Q2 - (mD * mD + mK * mK)) / mD; + double pk_sq = EK * EK - mK * mK; + if (pk_sq < 0) return 0.0; + return std::pow(GF, 2) / (24 * std::pow(M_PI, 3)) * ff2 * std::pow(std::sqrt(pk_sq), 3); + }; + + // Build an InteractionRecord to call the SIREN DifferentialDecayWidth + CharmMesonDecay decay(ParticleType::D0); + auto sigs = decay.GetPossibleSignaturesFromParent(ParticleType::D0); + auto sig = sigs[0]; // D0 -> K- e+ nu_e + + std::cout << "\n Q² | analytical | SIREN DDW | ratio" << std::endl; + double Q2_tests[] = {0.01, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0, 1.2, 1.35}; + for (double Q2 : Q2_tests) { + double anal = dGamma_analytical(Q2); + + // For SIREN DDW, we need to construct an InteractionRecord with the right kinematics + // DDW(InteractionRecord) reconstructs Q² from 4-momenta, so we need to set them up + // at the given Q². Use D rest frame for simplicity. + double EK_rest = (mD * mD + mK * mK - Q2) / (2 * mD); + double PK_rest = std::sqrt(std::max(0.0, EK_rest * EK_rest - mK * mK)); + + InteractionRecord rec; + rec.signature = sig; + rec.primary_mass = mD; + rec.primary_momentum = {mD, 0, 0, 0}; // D at rest + rec.target_mass = 0; + rec.secondary_momenta = { + {EK_rest, PK_rest, 0, 0}, // K along x + {0, 0, 0, 0}, // lepton (not used by DDW) + {0, 0, 0, 0} // neutrino (not used by DDW) + }; + rec.secondary_masses = {mK, Constants::electronMass, 0.0}; + + double siren_ddw_from_record = decay.DifferentialDecayWidth(rec); + + // Also call 4-arg DDW directly with same constants + std::vector my_constants = {F0CKM, alpha, ms_star}; + double siren_ddw_4arg = decay.DifferentialDecayWidth(my_constants, Q2, mD, mK); + + // And with alpha=0.44 (FormFactorFromRecord value) + std::vector ffr_constants = {0.719, 0.44, 2.00697}; + double siren_ddw_4arg_044 = decay.DifferentialDecayWidth(ffr_constants, Q2, mD, mK); + + std::cout << " Q2=" << Q2 + << " | anal=" << anal + << " | 4arg(a=0.50)=" << siren_ddw_4arg + << " | 4arg(a=0.44)=" << siren_ddw_4arg_044 + << " | fromRecord=" << siren_ddw_from_record + << " | ratioRec/anal=" << siren_ddw_from_record / anal + << std::endl; + } +} + +// ── Test 3: CharmMesonDecay SampleFinalState Q² ───────────────────────── + +TEST(CharmMesonDecay, SampledQ2Distribution) { + // Create D0 decay + CharmMesonDecay decay(ParticleType::D0); + auto sigs = decay.GetPossibleSignaturesFromParent(ParticleType::D0); + // sig[0] = D0 -> K- e+ nu_e + auto sig = sigs[0]; + + std::cout << "\nTest 3: CharmMesonDecay sampled Q² distribution" << std::endl; + std::cout << " Signature: D0 -> "; + for (auto s : sig.secondary_types) std::cout << (int)s << " "; + std::cout << std::endl; + + double mD = Constants::D0Mass; + double E_D = 100.0; // 100 GeV D meson + double p_D = std::sqrt(E_D * E_D - mD * mD); + + auto rng = std::make_shared(); + int Nsamp = 5000; + double sum_q2 = 0; + + for (int i = 0; i < Nsamp; ++i) { + InteractionRecord rec; + rec.signature = sig; + rec.primary_mass = mD; + rec.primary_momentum = {E_D, 0, 0, p_D}; + rec.primary_helicity = 0; + rec.target_mass = 0; + rec.target_helicity = 0; + rec.secondary_momenta = {{0,0,0,0}, {0,0,0,0}, {0,0,0,0}}; + rec.secondary_masses = {Constants::KminusMass, Constants::electronMass, 0.0}; + rec.secondary_helicities = {0, 0, 0}; + + CrossSectionDistributionRecord cdr(rec); + decay.SampleFinalState(cdr, rng); + cdr.Finalize(rec); + + // Reconstruct Q² = (p_D - p_K)² + double qE = rec.primary_momentum[0] - rec.secondary_momenta[0][0]; + double qpx = rec.primary_momentum[1] - rec.secondary_momenta[0][1]; + double qpy = rec.primary_momentum[2] - rec.secondary_momenta[0][2]; + double qpz = rec.primary_momentum[3] - rec.secondary_momenta[0][3]; + double Q2 = qE * qE - qpx * qpx - qpy * qpy - qpz * qpz; + sum_q2 += Q2; + } + + double mean_q2 = sum_q2 / Nsamp; + std::cout << " Mean Q² from SampleFinalState (" << Nsamp << " events): " << mean_q2 << std::endl; + std::cout << " Expected (correct): ~0.58" << std::endl; + + // If mean Q² > 0.7, the CDF sampling is biased + if (mean_q2 > 0.7) { + std::cout << " *** Q² BIAS CONFIRMED: " << mean_q2 << " >> 0.58 ***" << std::endl; + } + + // Loose check — correct value is 0.58, biased is ~0.79 + // This test documents the known bias rather than asserting correctness + EXPECT_GT(mean_q2, 0.3); // sanity: not negative/zero + EXPECT_LT(mean_q2, 1.2); // sanity: not absurdly high +} From 252d7bed2f3d19021f0449fe53847aaafebb6d19 Mon Sep 17 00:00:00 2001 From: Nicholas Kamp Date: Tue, 7 Apr 2026 12:52:21 -0400 Subject: [PATCH 19/93] partonic cross section updates --- .../interactions/private/DISFromSpline.cxx | 12 +- .../private/QuarkDISFromSpline.cxx | 132 +++++------------- 2 files changed, 39 insertions(+), 105 deletions(-) diff --git a/projects/interactions/private/DISFromSpline.cxx b/projects/interactions/private/DISFromSpline.cxx index 1518a54bc..5472af19f 100644 --- a/projects/interactions/private/DISFromSpline.cxx +++ b/projects/interactions/private/DISFromSpline.cxx @@ -92,7 +92,7 @@ DISFromSpline::DISFromSpline(std::string differential_filename, std::string tota InitializeSignatures(); SetUnits(units); } - + void DISFromSpline::SetUnits(std::string units) { std::transform(units.begin(), units.end(), units.begin(), [](unsigned char c){ return std::tolower(c); }); @@ -200,20 +200,18 @@ void DISFromSpline::ReadParamsFromSplineTable() { if(!mass_good) { if(int_good) { if(interaction_type_ == 1 or interaction_type_ == 2) { - target_mass_ = (siren::dataclasses::isLepton(siren::dataclasses::ParticleType::PPlus)+ - siren::dataclasses::isLepton(siren::dataclasses::ParticleType::Neutron))/2; + target_mass_ = siren::utilities::Constants::isoscalarMass; } else if(interaction_type_ == 3) { - target_mass_ = siren::dataclasses::isLepton(siren::dataclasses::ParticleType::EMinus); + target_mass_ = siren::utilities::Constants::electronMass; } else { throw std::runtime_error("Logic error. Interaction type is not 1, 2, or 3!"); } } else { if(differential_cross_section_.get_ndim() == 3) { - target_mass_ = (siren::dataclasses::isLepton(siren::dataclasses::ParticleType::PPlus)+ - siren::dataclasses::isLepton(siren::dataclasses::ParticleType::Neutron))/2; + target_mass_ = siren::utilities::Constants::isoscalarMass; } else if(differential_cross_section_.get_ndim() == 2) { - target_mass_ = siren::dataclasses::isLepton(siren::dataclasses::ParticleType::EMinus); + target_mass_ = siren::utilities::Constants::electronMass; } else { throw std::runtime_error("Logic error. Spline dimensionality is not 2, or 3!"); } diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 288c1c06f..e7d8532b4 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -129,7 +129,7 @@ QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::s InitializeSignatures(); SetUnits(units); } - + void QuarkDISFromSpline::SetUnits(std::string units) { std::transform(units.begin(), units.end(), units.begin(), [](unsigned char c){ return std::tolower(c); }); @@ -233,11 +233,11 @@ double QuarkDISFromSpline::getHadronMass(siren::dataclasses::ParticleType hadron case siren::dataclasses::ParticleType::DPlus: return( siren::utilities::Constants::DPlusMass); case siren::dataclasses::ParticleType::DMinus: - return( siren::utilities::Constants::DPlusMass); + return( siren::utilities::Constants::DPlusMass); case siren::dataclasses::ParticleType::Charm: return( siren::utilities::Constants::CharmMass); case siren::dataclasses::ParticleType::CharmBar: - return( siren::utilities::Constants::CharmMass); + return( siren::utilities::Constants::CharmMass); default: return(0.0); } @@ -291,10 +291,9 @@ void QuarkDISFromSpline::ReadParamsFromSplineTable() { if(!mass_good) { if(int_good) { if(interaction_type_ == 1 or interaction_type_ == 2) { - target_mass_ = (siren::dataclasses::isLepton(siren::dataclasses::ParticleType::PPlus)+ - siren::dataclasses::isLepton(siren::dataclasses::ParticleType::Neutron))/2; + target_mass_ = siren::utilities::Constants::isoscalarMass; } else if(interaction_type_ == 3) { - target_mass_ = siren::dataclasses::isLepton(siren::dataclasses::ParticleType::EMinus); + target_mass_ = siren::utilities::Constants::electronMass; } else { throw std::runtime_error("Logic error. Interaction type is not 1, 2, or 3!"); } @@ -302,7 +301,6 @@ void QuarkDISFromSpline::ReadParamsFromSplineTable() { } else { if(differential_cross_section_.get_ndim() == 3) { target_mass_ = siren::utilities::Constants::isoscalarMass; - } else if(differential_cross_section_.get_ndim() == 2) { target_mass_ = siren::utilities::Constants::electronMass; } else { @@ -351,10 +349,10 @@ void QuarkDISFromSpline::InitializeSignatures() { signature.secondary_types.push_back(siren::dataclasses::ParticleType::Hadrons); // define the charmed meson types based on the quark type, now considering only D0 and D+ if (quark_type_ == 1) { - D_types_ = {siren::dataclasses::Particle::ParticleType::D0, + D_types_ = {siren::dataclasses::Particle::ParticleType::D0, siren::dataclasses::Particle::ParticleType::DPlus}; } else { - D_types_ = {siren::dataclasses::Particle::ParticleType::D0Bar, + D_types_ = {siren::dataclasses::Particle::ParticleType::D0Bar, siren::dataclasses::Particle::ParticleType::DMinus}; } // push back the meson type @@ -370,7 +368,7 @@ void QuarkDISFromSpline::InitializeSignatures() { std::pair key(primary_type, target_type); signatures_by_parent_types_[key].push_back(full_signature); } - } + } } } @@ -426,7 +424,7 @@ void QuarkDISFromSpline::compute_cdf() { pdf_vector.push_back(0); - // set the spline table + // set the spline table siren::utilities::TableData1D inverse_cdf_data; inverse_cdf_data.x = cdf_vector; inverse_cdf_data.f = cdf_z_nodes; @@ -747,7 +745,7 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR if (pqx_lab>momq_lab){ // if current setting does not work, start looping through scalings - int maxIterations = 10; + int maxIterations = 10; int iteration = 0; double p1_lab_x = p1_lab.px(); double p1_lab_y = p1_lab.py(); @@ -760,7 +758,7 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); if (pqx_lab>momq_lab){ // std::cout << "triggered on " << momq_lab << " and " << pqx_lab << std::endl; - //scale down + //scale down E1_lab /= 10; E2_lab /= 10; p1_lab_x /= 10; @@ -805,7 +803,15 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR pq_lab.rotate(rand_rot); rk::P4 p3_lab((p1_lab - pq_lab).momentum(), m3); - rk::P4 p4_lab = p2_lab + pq_lab; + + double m_c = siren::utilities::Constants::CharmMass; + double xi = final_x * (1.0 + m_c * m_c / Q2); + if (xi >= 1.0) { + xi = 1.0 - 1e-5; // to avoid unphysical xi values due to numerical precision issues + } + rk::P4 p_parton(geom3::Vector3(0, 0, 0), xi * target_mass_); // parton at rest: (ξM, 0, 0, 0) + rk::P4 p4_lab = p_parton + pq_lab; // struck charm = ξ*p2 + q + rk::P4 p_spectator((1.0 - xi) * target_mass_, geom3::Vector3(0, 0, 0)); // spectator: ((1-ξ)M, 0,0, 0) rk::P4 p3; rk::P4 p4; @@ -813,109 +819,39 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR p4 = p4_lab; // momentum of the virtual charm // std::cout << "charm momentum is " << p4 << std::endl; - // compute the energy and 3-momentum of the virtual charm - // std::cout << "the virtual charm off-shell mass is " << p4.m() << std::endl; - double p3c = std::sqrt(std::pow(p4.px(), 2) + std::pow(p4.py(), 2) + std::pow(p4.pz(), 2)); - double Ec = p4.e(); //energy of primary charm - double mCH = getHadronMass(record.signature.secondary_types[meson_index]); // obtain charmed hadron mass - - // accept-reject sampling for a valid momentum fragmentation - bool frag_accept; - double randValue; - double z; - double ECH; + // D meson fragmentation: D gets fraction z of charm quark momentum + double mCH = getHadronMass(record.signature.secondary_types[meson_index]); + double p_charm_mag = std::sqrt(p4_lab.px()*p4_lab.px() + p4_lab.py()*p4_lab.py() + p4_lab.pz()*p4_lab.pz()); + geom3::Vector3 p4_mom = p4_lab.momentum(); + geom3::UnitVector3 p4_dir = p4_mom.direction(); - // add a maximum number of trials in the while loop + rk::P4 p4CH, p4X; int max_sampling = 500; int sampling = 0; - - // sample again if this eenrgy is not kinematically allowed - // this samples in the lab frame the energy of the D-meson such that mass is real do { sampling += 1; if (sampling > max_sampling) { - std::cout << "energy of the charm is " << Ec << " and momentum is " << p3c << std::endl; - std::cout << "desired mass of hadron is " << mCH << std::endl; - // throw(siren::utilities::InjectionFailure("Failed to sample hadronization!")); - break; + throw(siren::utilities::InjectionFailure("Failed to sample hadronization!")); } - randValue = random->Uniform(0,1); - z = inverseCdfTable(randValue); - ECH = z * Ec; - if (std::pow(ECH, 2) - std::pow(mCH, 2) <= 0) { - frag_accept = false; - } else { - frag_accept = true; - } - } while (!frag_accept); - // new attempt of using the isoscalar mass as the remnant hadronic shower mass - double mX = target_mass_; - double Mc = p4.m(); - // std::cout << "using remnant mass " << mX << std::endl; - // std::cout << "invariant charm mass and its energy is " << Mc << ", " << p4.e() << std::endl; - // std::cout << "target sampled D meson energy is " << ECH << std::endl; - // std::cout << "and the fraction of momentum is sampled to be " << z << std::endl; - //compute the energies in the charm rest frame - double E_CH_c = (std::pow(Mc, 2) - std::pow(mX, 2) + std::pow(mCH, 2)) / (2 * Mc); - // std::cout << "energy of charm in rest frame is " << E_CH_c << std::endl; - double p_c = std::sqrt((std::pow(Mc, 2) - std::pow(mCH + mX, 2)) * (std::pow(Mc, 2) - std::pow(mCH - mX, 2))) / (2 * Mc); - // std::cout << "momentum in charm rest frame is " << p_c << std::endl; - // compute the lorentz boost parameters - double gamma = p4.gamma(); - double beta = p4.beta(); - // std::cout << "beta and gamma parameters are " << beta << ", " << gamma << std::endl; - // using the lab frame fragmented energy and the - double cosTheta = std::max(std::min(((ECH - gamma * E_CH_c)/(gamma * beta * p_c)), 1.), -1.); - // std::cout << "cosine of theta in charm frame is " << cosTheta << std::endl; - // std::cout << "without cutting, the number is " << (ECH - gamma * E_CH_c)/(gamma * beta * p_c) << std::endl; - // now compute the momentum vectors in the rest frame - double sinTheta = std::sin(std::acos(cosTheta)); - // std::cout << "and sine of theta is computed to be " << sinTheta << std::endl; - rk::P4 p4CH_c(p_c * geom3::Vector3(cosTheta, sinTheta, 0), mCH); - rk::P4 p4X_c(p_c * geom3::Vector3(-cosTheta, -sinTheta, 0), mX); - // these all assume boost direction is charm direction. Now we should rotate back to charm lab momentum direction - geom3::Vector3 pc_lab_momentum = p4.momentum(); - geom3::UnitVector3 pc_lab_dir = pc_lab_momentum.direction(); - geom3::Rotation3 x_to_pc_lab_rot = geom3::rotationBetween(x_dir, pc_lab_dir); - p4X_c.rotate(x_to_pc_lab_rot); - p4CH_c.rotate(x_to_pc_lab_rot); - - // finally, we perform a random azimuthal rotation - double c_phi = random->Uniform(0, 2 * M_PI); - geom3::Rotation3 azimuth_rand_rot(pc_lab_dir, c_phi); - p4X_c.rotate(azimuth_rand_rot); - p4CH_c.rotate(azimuth_rand_rot); - - // and boost them back to the lab frame - rk::Boost boost_from_crest_to_lab = p4.labBoost(); - rk::P4 p4X = p4X_c.boost(boost_from_crest_to_lab); - rk::P4 p4CH = p4CH_c.boost(boost_from_crest_to_lab); - - // std::cout << "computed remnant mass and energy is " << p4X.m() << ", " << p4X.e() << std::endl; - // std::cout << "and computed D mass and energy is " << p4CH.m() << ", " << p4CH.e() << std::endl; - // std::cout << "target sampled D meson energy is " << ECH << std::endl; - - - // now we proceed to saving the final state kinematics + double z = inverseCdfTable(random->Uniform(0, 1)); + double pCH_mag = z * p_charm_mag; + p4CH = rk::P4(geom3::Vector3(p4_dir * pCH_mag), mCH); + p4X = p_spectator + p4_lab - p4CH; + } while (p4X.dot(p4X) < 0); + + // Save final state kinematics std::vector & secondaries = record.GetSecondaryParticleRecords(); siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[lepton_index]; siren::dataclasses::SecondaryParticleRecord & hadron = secondaries[hadron_index]; siren::dataclasses::SecondaryParticleRecord & meson = secondaries[meson_index]; - // std::cout << "QuarkDIS::SampleFInalState : the indices are: " << lepton_index << hadron_index<< meson_index << std::endl; lepton.SetFourMomentum({p3.e(), p3.px(), p3.py(), p3.pz()}); - // std::cout << "setting lepton mass with lepton momentum " << p3 << std::endl; lepton.SetMass(p3.m()); lepton.SetHelicity(record.primary_helicity); hadron.SetFourMomentum({p4X.e(), p4X.px(), p4X.py(), p4X.pz()}); - // std::cout << "setting hadron mass with hadron momentum " << p4X << std::endl; hadron.SetMass(p4X.m()); hadron.SetHelicity(record.target_helicity); meson.SetFourMomentum({p4CH.e(), p4CH.px(), p4CH.py(), p4CH.pz()}); - // std::cout << "setting meson mass with meson momentum " << p4CH << std::endl; - meson.SetMass(p4CH.m()); - meson.SetHelicity(record.target_helicity); // this needs working on - // std::cout << "finished sampling final state" << std::endl; } double QuarkDISFromSpline::FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const { From 435eaabeb70da1fcdc49fdc138a54d2cd85ee64e Mon Sep 17 00:00:00 2001 From: Nicholas Kamp Date: Tue, 7 Apr 2026 12:52:34 -0400 Subject: [PATCH 20/93] add kinematic gaurd to charm decay --- .../interactions/private/CharmMesonDecay.cxx | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx index 3a8a6641e..32f815dab 100644 --- a/projects/interactions/private/CharmMesonDecay.cxx +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -40,7 +40,7 @@ CharmMesonDecay::CharmMesonDecay() { } CharmMesonDecay::CharmMesonDecay(siren::dataclasses::Particle::ParticleType primary) { - + //standard stuff, constant across primary types std::vector constants; constants.resize(3); @@ -87,7 +87,7 @@ double CharmMesonDecay::particleMass(siren::dataclasses::ParticleType particle) case siren::dataclasses::ParticleType::DPlus: return( siren::utilities::Constants::DPlusMass); case siren::dataclasses::ParticleType::DMinus: - return( siren::utilities::Constants::DPlusMass); + return( siren::utilities::Constants::DPlusMass); case siren::dataclasses::ParticleType::K0: return( siren::utilities::Constants::K0Mass); case siren::dataclasses::ParticleType::K0Bar: @@ -95,7 +95,7 @@ double CharmMesonDecay::particleMass(siren::dataclasses::ParticleType particle) case siren::dataclasses::ParticleType::KPlus: return( siren::utilities::Constants::KplusMass); case siren::dataclasses::ParticleType::KMinus: - return( siren::utilities::Constants::KminusMass); + return( siren::utilities::Constants::KminusMass); case siren::dataclasses::ParticleType::EPlus: return( siren::utilities::Constants::electronMass ); case siren::dataclasses::ParticleType::EMinus: @@ -153,7 +153,7 @@ double CharmMesonDecay::TotalDecayWidthForFinalState(dataclasses::InteractionRec std::set kminus_muplus_numu = {siren::dataclasses::Particle::ParticleType::KMinus, siren::dataclasses::Particle::ParticleType::MuPlus, siren::dataclasses::Particle::ParticleType::NuMu}; - std::set hadrons = {siren::dataclasses::Particle::ParticleType::Hadrons}; + std::set hadrons = {siren::dataclasses::Particle::ParticleType::Hadrons}; if (primary == siren::dataclasses::Particle::ParticleType::DPlus) { tau = 1040 * (1e-15); if (secondaries == k0_eplus_nue) {branching_ratio = .1607;} // e+ semileptonic mode according to pdg @@ -176,7 +176,7 @@ std::vector CharmMesonDecay::GetPossibleSigna std::vector signatures; for(auto primary : primary_types) { std::vector new_signatures = GetPossibleSignaturesFromParent(primary); - signatures.insert(signatures.end(),new_signatures.begin(),new_signatures.end()); + signatures.insert(signatures.end(),new_signatures.begin(),new_signatures.end()); } return signatures; } @@ -268,7 +268,8 @@ double CharmMesonDecay::DifferentialDecayWidth(std::vector constants, do double Q2tilde = Q2 / (ms * ms); // compute the 3-momentum as a function of Q2 // double EK = 0.5 * (Q2 - pow(mD, 2) + pow(mK, 2)) / mD; // energy of Kaon - double EK = 0.5 * (Q2 - (pow(mD, 2) + pow(mK, 2))) / mD; // energy of Kaon + double EK = 0.5 * (pow(mD, 2) + pow(mK, 2) - Q2) / mD; // energy of Kaon + if (EK * EK < mK * mK) return 0.0; double PK = pow(pow(EK, 2) - pow(mK, 2), 0.5); // plug in the constants @@ -284,7 +285,7 @@ void CharmMesonDecay::computeDiffGammaCDF(std::vector constants, double std::function pdf = [&] (double x) -> double { return DifferentialDecayWidth(constants, x, mD, mK); }; - // first normalize the integral + // first normalize the integral double min = 0; double max = 1.4; // these set the min and max of the Q2 considered double normalization = siren::utilities::rombergIntegrate(pdf, min, max); @@ -330,8 +331,8 @@ void CharmMesonDecay::computeDiffGammaCDF(std::vector constants, double cdf_Q2_nodes.push_back(max); cdf_vector.push_back(1); pdf_vector.push_back(0); - - // set the spline table + + // set the spline table siren::utilities::TableData1D inverse_cdf_data; inverse_cdf_data.x = cdf_vector; inverse_cdf_data.f = cdf_Q2_nodes; @@ -366,7 +367,7 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco // first sample a q^2 double rand_value_for_Q2 = random->Uniform(0, 1); double Q2 = inverseCdf(rand_value_for_Q2); - + // now sample isotropically the "zenith" direction double cosTheta = random->Uniform(-1, 1); double sinTheta = std::sin(std::acos(cosTheta)); @@ -401,7 +402,7 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco // this ends the computation of D->W+K/Pi decay, now treat the W->l+nu decay double ml = particleMass(record.signature.secondary_types[1]); double mnu = 0; - double W_cosTheta = random->Uniform(-1, 1); // sampling the direction + double W_cosTheta = random->Uniform(-1, 1); // sampling the direction double W_sinTheta = std::sin(std::acos(W_cosTheta)); double El = (Q2 + pow(ml, 2)) / (2 * sqrt(Q2)); double Enu = (Q2 - pow(ml, 2)) / (2 * sqrt(Q2)); // the energies of the outgoing lepton and neutrino From 86a9e5611e4b1de4771e58d676ea3d3e02ebb62a Mon Sep 17 00:00:00 2001 From: Nicholas Kamp Date: Tue, 7 Apr 2026 13:04:41 -0400 Subject: [PATCH 21/93] keep old treatment as a comment --- .../private/QuarkDISFromSpline.cxx | 127 +++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index e7d8532b4..428483b63 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -804,6 +804,12 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR rk::P4 p3_lab((p1_lab - pq_lab).momentum(), m3); + + + // ############################################# + // New hadronization scheme: includes partonic cross section sampling and slow rescaling for charm mass effects + // ############################################## + double m_c = siren::utilities::Constants::CharmMass; double xi = final_x * (1.0 + m_c * m_c / Q2); if (xi >= 1.0) { @@ -817,7 +823,6 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR rk::P4 p4; p3 = p3_lab; // now we have our lepton momentum set, which should not be modified from here on p4 = p4_lab; // momentum of the virtual charm - // std::cout << "charm momentum is " << p4 << std::endl; // D meson fragmentation: D gets fraction z of charm quark momentum double mCH = getHadronMass(record.signature.secondary_types[meson_index]); @@ -852,6 +857,126 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR hadron.SetMass(p4X.m()); hadron.SetHelicity(record.target_helicity); meson.SetFourMomentum({p4CH.e(), p4CH.px(), p4CH.py(), p4CH.pz()}); + + + // ############################################# + // Included for posterity: original hadronization scheme + // ############################################## + + /* + + rk::P4 p4_lab = p2_lab + pq_lab; + + rk::P4 p3; + rk::P4 p4; + p3 = p3_lab; // now we have our lepton momentum set, which should not be modified from here on + p4 = p4_lab; // momentum of the virtual charm + + + // compute the energy and 3-momentum of the virtual charm + // std::cout << "the virtual charm off-shell mass is " << p4.m() << std::endl; + double p3c = std::sqrt(std::pow(p4.px(), 2) + std::pow(p4.py(), 2) + std::pow(p4.pz(), 2)); + double Ec = p4.e(); //energy of primary charm + double mCH = getHadronMass(record.signature.secondary_types[meson_index]); // obtain charmed hadron mass + + // accept-reject sampling for a valid momentum fragmentation + bool frag_accept; + double randValue; + double z; + double ECH; + + // add a maximum number of trials in the while loop + int max_sampling = 500; + int sampling = 0; + + // sample again if this eenrgy is not kinematically allowed + // this samples in the lab frame the energy of the D-meson such that mass is real + do { + sampling += 1; + if (sampling > max_sampling) { + std::cout << "energy of the charm is " << Ec << " and momentum is " << p3c << std::endl; + std::cout << "desired mass of hadron is " << mCH << std::endl; + // throw(siren::utilities::InjectionFailure("Failed to sample hadronization!")); + break; + } + randValue = random->Uniform(0,1); + z = inverseCdfTable(randValue); + ECH = z * Ec; + if (std::pow(ECH, 2) - std::pow(mCH, 2) <= 0) { + frag_accept = false; + } else { + frag_accept = true; + } + } while (!frag_accept); + // new attempt of using the isoscalar mass as the remnant hadronic shower mass + double mX = target_mass_; + double Mc = p4.m(); + // std::cout << "using remnant mass " << mX << std::endl; + // std::cout << "invariant charm mass and its energy is " << Mc << ", " << p4.e() << std::endl; + // std::cout << "target sampled D meson energy is " << ECH << std::endl; + // std::cout << "and the fraction of momentum is sampled to be " << z << std::endl; + //compute the energies in the charm rest frame + double E_CH_c = (std::pow(Mc, 2) - std::pow(mX, 2) + std::pow(mCH, 2)) / (2 * Mc); + // std::cout << "energy of charm in rest frame is " << E_CH_c << std::endl; + double p_c = std::sqrt((std::pow(Mc, 2) - std::pow(mCH + mX, 2)) * (std::pow(Mc, 2) - std::pow(mCH - mX, 2))) / (2 * Mc); + // std::cout << "momentum in charm rest frame is " << p_c << std::endl; + // compute the lorentz boost parameters + double gamma = p4.gamma(); + double beta = p4.beta(); + // std::cout << "beta and gamma parameters are " << beta << ", " << gamma << std::endl; + // using the lab frame fragmented energy and the + double cosTheta = std::max(std::min(((ECH - gamma * E_CH_c)/(gamma * beta * p_c)), 1.), -1.); + // std::cout << "cosine of theta in charm frame is " << cosTheta << std::endl; + // std::cout << "without cutting, the number is " << (ECH - gamma * E_CH_c)/(gamma * beta * p_c) << std::endl; + // now compute the momentum vectors in the rest frame + double sinTheta = std::sin(std::acos(cosTheta)); + // std::cout << "and sine of theta is computed to be " << sinTheta << std::endl; + rk::P4 p4CH_c(p_c * geom3::Vector3(cosTheta, sinTheta, 0), mCH); + rk::P4 p4X_c(p_c * geom3::Vector3(-cosTheta, -sinTheta, 0), mX); + // these all assume boost direction is charm direction. Now we should rotate back to charm lab momentum direction + geom3::Vector3 pc_lab_momentum = p4.momentum(); + geom3::UnitVector3 pc_lab_dir = pc_lab_momentum.direction(); + geom3::Rotation3 x_to_pc_lab_rot = geom3::rotationBetween(x_dir, pc_lab_dir); + p4X_c.rotate(x_to_pc_lab_rot); + p4CH_c.rotate(x_to_pc_lab_rot); + + // finally, we perform a random azimuthal rotation + double c_phi = random->Uniform(0, 2 * M_PI); + geom3::Rotation3 azimuth_rand_rot(pc_lab_dir, c_phi); + p4X_c.rotate(azimuth_rand_rot); + p4CH_c.rotate(azimuth_rand_rot); + + // and boost them back to the lab frame + rk::Boost boost_from_crest_to_lab = p4.labBoost(); + rk::P4 p4X = p4X_c.boost(boost_from_crest_to_lab); + rk::P4 p4CH = p4CH_c.boost(boost_from_crest_to_lab); + + // std::cout << "computed remnant mass and energy is " << p4X.m() << ", " << p4X.e() << std::endl; + // std::cout << "and computed D mass and energy is " << p4CH.m() << ", " << p4CH.e() << std::endl; + // std::cout << "target sampled D meson energy is " << ECH << std::endl; + + + // now we proceed to saving the final state kinematics + std::vector & secondaries = record.GetSecondaryParticleRecords(); + siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[lepton_index]; + siren::dataclasses::SecondaryParticleRecord & hadron = secondaries[hadron_index]; + siren::dataclasses::SecondaryParticleRecord & meson = secondaries[meson_index]; + // std::cout << "QuarkDIS::SampleFInalState : the indices are: " << lepton_index << hadron_index<< meson_index << std::endl; + + lepton.SetFourMomentum({p3.e(), p3.px(), p3.py(), p3.pz()}); + // std::cout << "setting lepton mass with lepton momentum " << p3 << std::endl; + lepton.SetMass(p3.m()); + lepton.SetHelicity(record.primary_helicity); + hadron.SetFourMomentum({p4X.e(), p4X.px(), p4X.py(), p4X.pz()}); + // std::cout << "setting hadron mass with hadron momentum " << p4X << std::endl; + hadron.SetMass(p4X.m()); + hadron.SetHelicity(record.target_helicity); + meson.SetFourMomentum({p4CH.e(), p4CH.px(), p4CH.py(), p4CH.pz()}); + // std::cout << "setting meson mass with meson momentum " << p4CH << std::endl; + meson.SetMass(p4CH.m()); + meson.SetHelicity(record.target_helicity); // this needs working on + // std::cout << "finished sampling final state" << std::endl; + */ } double QuarkDISFromSpline::FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const { From b234445cce0a0155cb5c6cfd7ffd53af618c02c5 Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Mon, 9 Mar 2026 15:12:10 -0400 Subject: [PATCH 22/93] fixed repeating seed issue --- projects/utilities/private/Random.cxx | 6 +++--- projects/utilities/public/SIREN/utilities/Random.h | 13 ++++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/projects/utilities/private/Random.cxx b/projects/utilities/private/Random.cxx index 78a2e0f2f..4f4701197 100644 --- a/projects/utilities/private/Random.cxx +++ b/projects/utilities/private/Random.cxx @@ -9,13 +9,13 @@ namespace utilities { SIREN_random::SIREN_random(void){ // default to boring seed seed = 1; - configuration = std::default_random_engine(seed); + configuration = std::mt19937_64(seed); generator = std::uniform_real_distribution( 0.0, 1.0); } SIREN_random::SIREN_random( unsigned int _seed ){ seed = _seed; - configuration = std::default_random_engine(seed); + configuration = std::mt19937_64(seed); generator = std::uniform_real_distribution( 0.0, 1.0); } @@ -42,7 +42,7 @@ namespace utilities { // reconfigures the generator with a new seed void SIREN_random::set_seed( unsigned int new_seed) { seed = new_seed; - this->configuration = std::default_random_engine(seed); + this->configuration = std::mt19937_64(seed); } } // namespace utilities diff --git a/projects/utilities/public/SIREN/utilities/Random.h b/projects/utilities/public/SIREN/utilities/Random.h index f6ae1e9db..cd06c96f8 100644 --- a/projects/utilities/public/SIREN/utilities/Random.h +++ b/projects/utilities/public/SIREN/utilities/Random.h @@ -7,7 +7,7 @@ // this implements a class to sample numbers just like in an i3 service -#include // default_random_engine, uniform_real_distribution +#include // mt19937_64, uniform_real_distribution #include #include @@ -29,7 +29,7 @@ namespace utilities { // this naming convention is used to double Uniform( double from=0.0, double to=1.0); - double PowerLaw(double min, double max, double n); + double PowerLaw(double min, double max, double n); // in case this is set up without a seed! void set_seed(unsigned int new_seed); @@ -55,7 +55,14 @@ namespace utilities { private: unsigned int seed; - std::default_random_engine configuration; + // Previously used std::default_random_engine (minstd_rand0 on GCC), + // a linear congruential generator with only ~2.1 billion states + // (period 2^31-2). With ~500 RNG draws per event and thousands of + // seeds each generating 10k events, the birthday paradox causes + // frequent internal state collisions across seeds, producing + // identical event sequences. Switched to Mersenne Twister + // (period 2^19937-1) to eliminate cross-seed duplicates. + std::mt19937_64 configuration; std::uniform_real_distribution generator; }; From 2f57b2c40c2cbc4a0bddd675209f6af78b929c1c Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Mon, 13 Apr 2026 23:22:31 -0400 Subject: [PATCH 23/93] incorporate Pavel's PR: new 3 body decay class and PythiaDISCrossSection class --- projects/interactions/CMakeLists.txt | 18 +- .../private/CharmDISFromSpline.cxx | 2 +- .../interactions/private/CharmMesonDecay.cxx | 2 +- .../private/CharmMesonDecay3Body.cxx | 515 +++++++++++++++ .../private/PythiaDISCrossSection.cxx | 607 ++++++++++++++++++ .../private/pybindings/CharmMesonDecay3Body.h | 34 + .../pybindings/PythiaDISCrossSection.h | 70 ++ .../private/pybindings/interactions.cxx | 6 + .../SIREN/interactions/CharmMesonDecay3Body.h | 101 +++ .../interactions/PythiaDISCrossSection.h | 221 +++++++ 10 files changed, 1573 insertions(+), 3 deletions(-) create mode 100644 projects/interactions/private/CharmMesonDecay3Body.cxx create mode 100644 projects/interactions/private/PythiaDISCrossSection.cxx create mode 100644 projects/interactions/private/pybindings/CharmMesonDecay3Body.h create mode 100644 projects/interactions/private/pybindings/PythiaDISCrossSection.h create mode 100644 projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h create mode 100644 projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h diff --git a/projects/interactions/CMakeLists.txt b/projects/interactions/CMakeLists.txt index 203816009..51403867a 100644 --- a/projects/interactions/CMakeLists.txt +++ b/projects/interactions/CMakeLists.txt @@ -20,16 +20,28 @@ LIST (APPEND interactions_SOURCES ${PROJECT_SOURCE_DIR}/projects/interactions/private/Hadronization.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/CharmHadronization.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/CharmMesonDecay.cxx + ${PROJECT_SOURCE_DIR}/projects/interactions/private/CharmMesonDecay3Body.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/DMesonELoss.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/QuarkDISFromSpline.cxx + ${PROJECT_SOURCE_DIR}/projects/interactions/private/PythiaDISCrossSection.cxx ) add_library(SIREN_interactions OBJECT ${interactions_SOURCES}) set_property(TARGET SIREN_interactions PROPERTY POSITION_INDEPENDENT_CODE ON) + +# Pythia8 + LHAPDF for PythiaDISCrossSection +# Defaults target our cluster installs; override via -DPYTHIA8_DIR=... on cmake cmdline +set(PYTHIA8_DIR "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/miaochenjin/local/pythia8315" + CACHE PATH "Pythia8 installation root") +set(LHAPDF_DIR "/cvmfs/icecube.opensciencegrid.org/py3-v4.1.1/RHEL_8_x86_64" + CACHE PATH "LHAPDF installation root") + target_include_directories(SIREN_interactions PUBLIC $ $ ${PYTHON_INCLUDE_DIRS} + ${PYTHIA8_DIR}/include + ${LHAPDF_DIR}/include ) target_link_libraries(SIREN_interactions @@ -45,6 +57,9 @@ target_link_libraries(SIREN_interactions SIREN_detector ) +target_link_directories(SIREN_interactions PUBLIC ${PYTHIA8_DIR}/lib ${LHAPDF_DIR}/lib) +target_link_libraries(SIREN_interactions PUBLIC pythia8 LHAPDF dl pthread) + install(DIRECTORY "${PROJECT_SOURCE_DIR}/projects/interactions/public/" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} FILES_MATCHING @@ -57,7 +72,8 @@ package_add_test(UnitTest_DipoleFromTable ${PROJECT_SOURCE_DIR}/projects/interac #package_add_test(UnitTest_ElasticScattering ${PROJECT_SOURCE_DIR}/projects/interactions/private/test/ElasticScattering_TEST.cxx) pybind11_add_module(interactions ${PROJECT_SOURCE_DIR}/projects/interactions/private/pybindings/interactions.cxx) -target_link_libraries(interactions PRIVATE SIREN photospline rk_static pybind11::embed) +target_link_directories(interactions PRIVATE ${PYTHIA8_DIR}/lib ${LHAPDF_DIR}/lib) +target_link_libraries(interactions PRIVATE SIREN photospline rk_static pybind11::embed pythia8 LHAPDF dl pthread) #pybind11_add_module(pyDarkNewsSerializer ${PROJECT_SOURCE_DIR}/projects/interactions/private/pybindings/pyDarkNewsSerializer.cxx) #target_link_libraries(pyDarkNewsSerializer PRIVATE SIREN photospline rk_static pybind11::embed) if(DEFINED SKBUILD) diff --git a/projects/interactions/private/CharmDISFromSpline.cxx b/projects/interactions/private/CharmDISFromSpline.cxx index 27921cb33..539eda0f6 100644 --- a/projects/interactions/private/CharmDISFromSpline.cxx +++ b/projects/interactions/private/CharmDISFromSpline.cxx @@ -472,7 +472,7 @@ void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR // kin_vars and its twin are 3-vectors containing [nu-energy, Bjorken X, Bjorken Y] std::array kin_vars, test_kin_vars; - // centers of the cross section spline tales. + // centers of the cross section spline tables. std::array spline_table_center, test_spline_table_center; // values of cross_section from the splines. By * Bx * Spline(E,x,y) diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx index 32f815dab..ac1306f10 100644 --- a/projects/interactions/private/CharmMesonDecay.cxx +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -236,7 +236,7 @@ std::vector CharmMesonDecay::FormFactorFromRecord(dataclasses::CrossSect constants[2] = 2.01027; // this is excited charged D meson } else if (signature.primary_type == dataclasses::Particle::ParticleType::D0 && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::KMinus) { constants[0] = 0.719; // this is f^+(0)|V_cs| for neutral D - constants[1] = 0.44; // this is alpha, same for all K final states + constants[1] = 0.50; // this is alpha, same for all K final states constants[2] = 2.00697; // this is excited neutral D meson } return constants; diff --git a/projects/interactions/private/CharmMesonDecay3Body.cxx b/projects/interactions/private/CharmMesonDecay3Body.cxx new file mode 100644 index 000000000..c0bd553e8 --- /dev/null +++ b/projects/interactions/private/CharmMesonDecay3Body.cxx @@ -0,0 +1,515 @@ +#include "SIREN/interactions/CharmMesonDecay3Body.h" + +#include + +#include +#include + +#include "SIREN/dataclasses/InteractionRecord.h" // for Interac... +#include "SIREN/dataclasses/InteractionSignature.h" // for Interac... +#include "SIREN/dataclasses/Particle.h" // for Particle +#include "SIREN/math/Vector3D.h" // for Vector3D +#include "SIREN/utilities/Constants.h" // for GeV, pi +#include "SIREN/utilities/Random.h" // for SIREN_random + +#include "SIREN/utilities/Integration.h" // for rombergInt... +#include "SIREN/utilities/Interpolator.h" + +#include "SIREN/interactions/Decay.h" + +namespace siren { +namespace interactions { + +CharmMesonDecay3Body::CharmMesonDecay3Body() { + // this is the default initialization but should never be used + + // we need to compute cdf here b/c otherwise SampleFinalState becomes non constant + // in this case, we will need to compute all the possible dGamma's here + // maybe add a map here, but for now we hard code first + std::vector constants; + constants.resize(3); + // check the primary and secondaries of the signature + constants[0] = 0.725; // this is f^+(0)|V_cs| for charged D + constants[1] = 0.44; // this is alpha, same for all K final states + constants[2] = 2.01027; // this is excited charged D meson + + double mD = particleMass(siren::dataclasses::Particle::ParticleType::DPlus); + double mK = particleMass(siren::dataclasses::Particle::ParticleType::K0Bar); + + computeDiffGammaCDF(constants, mD, mK); +} + +CharmMesonDecay3Body::CharmMesonDecay3Body(siren::dataclasses::Particle::ParticleType primary) { + + //standard stuff, constant across primary types + std::vector constants; + constants.resize(3); + double mD; + double mK; + + if (primary == siren::dataclasses::Particle::ParticleType::DPlus) { + constants[0] = 0.725; // this is f^+(0)|V_cs| for charged D + constants[1] = 0.44; // this is alpha, same for all K final states + constants[2] = 2.01027; // this is excited charged D meson + + mD = particleMass(siren::dataclasses::Particle::ParticleType::DPlus); + mK = particleMass(siren::dataclasses::Particle::ParticleType::K0Bar); + + } else if (primary == siren::dataclasses::Particle::ParticleType::D0) { + constants[0] = 0.719; // this is f^+(0)|V_cs| for charged D + constants[1] = 0.50; // this is alpha, same for all K final states + constants[2] = 2.00697; // this is excited charged D meson + + mD = particleMass(siren::dataclasses::Particle::ParticleType::D0); + mK = particleMass(siren::dataclasses::Particle::ParticleType::KMinus); + } + + computeDiffGammaCDF(constants, mD, mK); + +} + +bool CharmMesonDecay3Body::equal(Decay const & other) const { + const CharmMesonDecay3Body* x = dynamic_cast(&other); + + if(!x) + return false; + else + return primary_types == x->primary_types; +} + + +double CharmMesonDecay3Body::particleMass(siren::dataclasses::ParticleType particle) { + switch(particle){ + case siren::dataclasses::ParticleType::D0: + return( siren::utilities::Constants::D0Mass); + case siren::dataclasses::ParticleType::D0Bar: + return( siren::utilities::Constants::D0Mass); + case siren::dataclasses::ParticleType::DPlus: + return( siren::utilities::Constants::DPlusMass); + case siren::dataclasses::ParticleType::DMinus: + return( siren::utilities::Constants::DPlusMass); + case siren::dataclasses::ParticleType::K0: + return( siren::utilities::Constants::K0Mass); + case siren::dataclasses::ParticleType::K0Bar: + return( siren::utilities::Constants::K0Mass); + case siren::dataclasses::ParticleType::KPlus: + return( siren::utilities::Constants::KplusMass); + case siren::dataclasses::ParticleType::KMinus: + return( siren::utilities::Constants::KminusMass); + case siren::dataclasses::ParticleType::EPlus: + return( siren::utilities::Constants::electronMass ); + case siren::dataclasses::ParticleType::EMinus: + return( siren::utilities::Constants::electronMass ); + case siren::dataclasses::ParticleType::MuPlus: + return( siren::utilities::Constants::muonMass ); + case siren::dataclasses::ParticleType::MuMinus: + return( siren::utilities::Constants::muonMass ); + case siren::dataclasses::ParticleType::TauPlus: + return( siren::utilities::Constants::tauMass ); + case siren::dataclasses::ParticleType::TauMinus: + return( siren::utilities::Constants::tauMass ); + default: + return(0.0); + } +} + +double CharmMesonDecay3Body::TotalDecayWidth(dataclasses::InteractionRecord const & record) const { + return TotalDecayWidth(record.signature.primary_type); +} + +// in this implementation, should we take total decay width to be only the channels we considered? +double CharmMesonDecay3Body::TotalDecayWidth(siren::dataclasses::Particle::ParticleType primary) const { + double total_width = 0; + std::vector possible_signatures = GetPossibleSignaturesFromParent(primary); + for (auto sig : possible_signatures) { + // make a fake record and full from total decay width for final state + siren::dataclasses::InteractionRecord fake_record; + fake_record.signature = sig; + double this_width = TotalDecayWidthForFinalState(fake_record); + total_width += this_width; + } + return total_width; +} + +// current problem: in implementation we see kaons and pions both as hadrons, but they should have different branching ratios and form factors +double CharmMesonDecay3Body::TotalDecayWidthForFinalState(dataclasses::InteractionRecord const & record) const { + double branching_ratio; + double tau; // total lifetime for all visible and invisible modes + // read in the signature and types + siren::dataclasses::Particle::ParticleType primary = record.signature.primary_type; + std::vector secondaries_vector = record.signature.secondary_types; + std::set secondaries = std::set(secondaries_vector.begin(), secondaries_vector.end()); + + // define the decay modes + std::set k0_eplus_nue = {siren::dataclasses::Particle::ParticleType::K0Bar, + siren::dataclasses::Particle::ParticleType::EPlus, + siren::dataclasses::Particle::ParticleType::NuE}; + std::set kminus_eplus_nue = {siren::dataclasses::Particle::ParticleType::KMinus, + siren::dataclasses::Particle::ParticleType::EPlus, + siren::dataclasses::Particle::ParticleType::NuE}; + std::set k0_muplus_numu = {siren::dataclasses::Particle::ParticleType::K0Bar, + siren::dataclasses::Particle::ParticleType::MuPlus, + siren::dataclasses::Particle::ParticleType::NuMu}; + std::set kminus_muplus_numu = {siren::dataclasses::Particle::ParticleType::KMinus, + siren::dataclasses::Particle::ParticleType::MuPlus, + siren::dataclasses::Particle::ParticleType::NuMu}; + std::set hadrons = {siren::dataclasses::Particle::ParticleType::Hadrons}; + if (primary == siren::dataclasses::Particle::ParticleType::DPlus) { + tau = 1040 * (1e-15); + if (secondaries == k0_eplus_nue) {branching_ratio = .1607;} // e+ semileptonic mode according to pdg + else if (secondaries == k0_muplus_numu) {branching_ratio = .176;} // mu+ anything according to pdg + else if (secondaries == hadrons) {branching_ratio = (1 - .1607 - .176);} // everything else + } else if (primary == siren::dataclasses::Particle::ParticleType::D0) { + tau = 410.1 * (1e-15); + if (secondaries == kminus_eplus_nue) {branching_ratio = .0649;} // e+ semileptonic mode according to pdg + else if (secondaries == kminus_muplus_numu) {branching_ratio = .067;} // mu+ anything according to pdg + else if (secondaries == hadrons) {branching_ratio = (1 - .0649 - .067);} // everything else + } + else { + std::cout << "this decay mode is not yet implemented!" << std::endl; + } + return branching_ratio * siren::utilities::Constants::hbar / tau * siren::utilities::Constants::GeV; +} + + +std::vector CharmMesonDecay3Body::GetPossibleSignatures() const { + std::vector signatures; + for(auto primary : primary_types) { + std::vector new_signatures = GetPossibleSignaturesFromParent(primary); + signatures.insert(signatures.end(),new_signatures.begin(),new_signatures.end()); + } + return signatures; +} + +std::vector CharmMesonDecay3Body::GetPossibleSignaturesFromParent(siren::dataclasses::Particle::ParticleType primary) const { + std::vector signatures; + // initialize semileptonic signatures + dataclasses::InteractionSignature semilep_signature; + semilep_signature.primary_type = primary; + semilep_signature.target_type = siren::dataclasses::Particle::ParticleType::Decay; + semilep_signature.secondary_types.resize(3); + // initialize other signatures + dataclasses::InteractionSignature hadron_signature; + hadron_signature.primary_type = primary; + hadron_signature.target_type = siren::dataclasses::Particle::ParticleType::Decay; + hadron_signature.secondary_types.resize(1); + + if (primary==siren::dataclasses::Particle::ParticleType::DPlus) { + // semi-leptonic modes with muon and electron + semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::K0Bar; + semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::EPlus; + semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuE; + signatures.push_back(semilep_signature); + semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::K0Bar; + semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::MuPlus; + semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuMu; + signatures.push_back(semilep_signature); + // all other modes implemented as one big bang + hadron_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; + signatures.push_back(hadron_signature); + } else if (primary==siren::dataclasses::Particle::ParticleType::D0) { + semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::KMinus; + semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::EPlus; + semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuE; + signatures.push_back(semilep_signature); + semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::KMinus; + semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::MuPlus; + semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuMu; + signatures.push_back(semilep_signature); + hadron_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; + signatures.push_back(hadron_signature); + } + else { + std::cout << "this D meson decay has not been implemented yet" << std::endl; + } + return signatures; +} + +std::vector CharmMesonDecay3Body::FormFactorFromRecord(dataclasses::CrossSectionDistributionRecord const & record) const { + dataclasses::InteractionSignature signature = record.signature; + std::vector constants; + constants.resize(3); + // check the primary and secondaries of the signature + if (signature.primary_type == dataclasses::Particle::ParticleType::DPlus && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::K0Bar) { + constants[0] = 0.725; // this is f^+(0)|V_cs| for charged D + constants[1] = 0.44; // this is alpha, same for all K final states + constants[2] = 2.01027; // this is excited charged D meson + } else if (signature.primary_type == dataclasses::Particle::ParticleType::D0 && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::KMinus) { + constants[0] = 0.719; // this is f^+(0)|V_cs| for neutral D + constants[1] = 0.50; // this is alpha, same for all K final states + constants[2] = 2.00697; // this is excited neutral D meson + } + return constants; +} + +double CharmMesonDecay3Body::DifferentialDecayWidth(dataclasses::InteractionRecord const & record) const { + // first let the fully hadronic state be handled separately + dataclasses::InteractionSignature signature = record.signature; + if (signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::Hadrons) { + return TotalDecayWidthForFinalState(record); + } + // get the form factor constants + std::vector constants = FormFactorFromRecord(record); + // calculate the q^2 + rk::P4 pD(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); + rk::P4 pKPi(geom3::Vector3(record.secondary_momenta[0][1], record.secondary_momenta[0][2], record.secondary_momenta[0][3]), record.secondary_masses[0]); + double Q2 = (pD - pKPi).dot(pD - pKPi); + // primary and secondary masses are also needed + double mD = record.primary_mass; + double mK = record.secondary_masses[0]; + return DifferentialDecayWidth(constants, Q2, mD, mK); +} + +double CharmMesonDecay3Body::DifferentialDecayWidth(std::vector constants, double Q2, double mD, double mK) const { + // get the numerical constants from the vector + double F0CKM = constants[0]; + double alpha = constants[1]; + double ms = constants[2]; + double Q2tilde = Q2 / (ms * ms); + // compute the 3-momentum as a function of Q2 + // double EK = 0.5 * (Q2 - pow(mD, 2) + pow(mK, 2)) / mD; // energy of Kaon + double EK = 0.5 * (pow(mD, 2) + pow(mK, 2) - Q2) / mD; // energy of Kaon + if (EK * EK < mK * mK) return 0.0; + + double PK = pow(pow(EK, 2) - pow(mK, 2), 0.5); + // plug in the constants + double dGamma = pow(siren::utilities::Constants::FermiConstant,2) / (24 * pow(siren::utilities::Constants::pi,3)) * pow(F0CKM,2) * + pow((1/((1-Q2tilde) * (1 - alpha * Q2tilde))),2) * pow(PK,3); + return dGamma; +} + +void CharmMesonDecay3Body::computeDiffGammaCDF(std::vector constants, double mD, double mK) { + + // returns a 1D interpolater table for dGamma cdf + // define the pdf with only Q2 as the input + std::function pdf = [&] (double x) -> double { + return DifferentialDecayWidth(constants, x, mD, mK); + }; + // first normalize the integral + double min = 0; + double max = 1.4; // these set the min and max of the Q2 considered + double normalization = siren::utilities::rombergIntegrate(pdf, min, max); + std::function normed_pdf = [&] (double x) -> double { + return DifferentialDecayWidth(constants, x, mD, mK) / normalization; + }; + // now create the spline and compute the CDF + + // set the Q2 nodes (use 100 nodes) + std::vector Q2spline; + for (int i = 0; i < 100; ++i) { + Q2spline.push_back(0.01 + i * (max-min) / 100 ); + } + + // declare the cdf vectors + std::vector cdf_vector; + std::vector cdf_Q2_nodes; + std::vector pdf_vector; + + cdf_Q2_nodes.push_back(0); + cdf_vector.push_back(0); + pdf_vector.push_back(0); + + // compute the spline table + for (int i = 0; i < Q2spline.size(); ++i) { + if (i == 0) { + double cur_Q2 = Q2spline[i]; + double cur_pdf = normed_pdf(cur_Q2); + double area = cur_Q2 * cur_pdf * 0.5; + pdf_vector.push_back(cur_pdf); + cdf_vector.push_back(area); + cdf_Q2_nodes.push_back(cur_Q2); + continue; + } + double cur_Q2 = Q2spline[i]; + double cur_pdf = normed_pdf(cur_Q2); + double area = 0.5 * (pdf_vector[i - 1] + cur_pdf) * (Q2spline[i] - Q2spline[i - 1]); + pdf_vector.push_back(cur_pdf); + cdf_Q2_nodes.push_back(cur_Q2); + cdf_vector.push_back(area + cdf_vector.back()); + } + + cdf_Q2_nodes.push_back(max); + cdf_vector.push_back(1); + pdf_vector.push_back(0); + + // set the spline table + siren::utilities::TableData1D inverse_cdf_data; + inverse_cdf_data.x = cdf_vector; + inverse_cdf_data.f = cdf_Q2_nodes; + + inverseCdf = siren::utilities::Interpolator1D(inverse_cdf_data); + return; + +} + +// this is temporary implementation +void CharmMesonDecay3Body::SampleFinalStateHadronic(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { + std::vector & secondaries = record.GetSecondaryParticleRecords(); + siren::dataclasses::SecondaryParticleRecord & hadrons = secondaries[0]; + rk::P4 p4D_lab(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); + hadrons.SetFourMomentum({p4D_lab.e(), p4D_lab.px(), p4D_lab.py(), p4D_lab.pz()}); + hadrons.SetMass(p4D_lab.m()); + hadrons.SetHelicity(record.primary_helicity); +} + +void CharmMesonDecay3Body::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { + // Hadronic decay branch — identical to the 2-body class. + dataclasses::InteractionSignature signature = record.signature; + if (signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::Hadrons) { + SampleFinalStateHadronic(record, random); + return; + } + + // ========================================================================= + // 3-body phase space sampling following Pythia's approach + // (ParticleDecays::threeBody in ParticleDecays.cc) + // + // D (m0) -> K (m1) + lepton (m2) + neutrino (m3) + // + // Phase space: sample m23 (lepton-neutrino invariant mass = sqrt(q^2)) + // flat in allowed range, accept-reject on phase space weight. + // Then apply V-A matrix element correction. + // + // K / K*(892) mixing: a fraction (1 - fracK) of events use mK*=0.892 GeV + // as the hadron mass instead of the pseudoscalar K mass, which broadens + // the lepton spectrum through phase-space and matrix-element effects. + // + // NOTE (design decision): this is *kinematic* K/K* mixing only. The + // secondary particle's ParticleType is always left at the K species from + // the signature (K0bar / K-), even when the K* mass was drawn. We do not + // advertise separate K* signatures in GetPossibleSignaturesFromParent() + // and we do not re-type the secondary. This is intentional — for our use + // case (weighting lepton/neutrino kinematics correctly in the presence of + // the resonant K* contribution) the mass treatment is what matters, and + // downstream propagation treats the secondary as a pseudoscalar K. + // ========================================================================= + + double mD = particleMass(record.signature.primary_type); + double mK_base = particleMass(record.signature.secondary_types[0]); // K mass from signature + double ml = particleMass(record.signature.secondary_types[1]); // lepton mass + double mnu = 0.0; // (massless) neutrino + + // K/K* mixing fractions from PDG semileptonic branching ratios + double mKstar = 0.89166; // K*(892) mass [GeV] + double fracK; + if (record.signature.primary_type == siren::dataclasses::Particle::ParticleType::DPlus) { + // D+ -> Kbar0 l nu: BR_K = 8.74%, BR_K*bar = 5.33% + fracK = 8.74 / (8.74 + 5.33); + } else { + // D0 -> K- l nu: BR_K = 3.41%, BR_K*- = 2.17% + fracK = 3.41 / (3.41 + 2.17); + } + double mK = (random->Uniform(0, 1) < fracK) ? mK_base : mKstar; + + // D meson 4-momentum in lab frame (needed for final lab-frame boost) + rk::P4 p4D_lab(geom3::Vector3(record.primary_momentum[1], + record.primary_momentum[2], + record.primary_momentum[3]), + record.primary_mass); + + // Phase-space limits for m23 = sqrt(q^2) = (lepton+nu) invariant mass + double m23Min = ml + mnu; + double m23Max = mD - mK; + double mDiff = m23Max - m23Min; + + // Kinematic envelope used as the accept-reject ceiling + double p1Max = 0.5 * std::sqrt((mD - mK - m23Min) * (mD + mK + m23Min) + * (mD + mK - m23Min) * (mD - mK + m23Min)) / mD; + double p23Max = 0.5 * std::sqrt((m23Max - ml - mnu) * (m23Max + ml + mnu) + * (m23Max + ml - mnu) * (m23Max - ml + mnu)) / m23Max; + double wtPSmax = 0.5 * p1Max * p23Max; + + // V-A matrix element upper bound (Pythia meMode == 22) + double wtMEmax = std::min(std::pow(mD, 4) / 16.0, + mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); + + rk::P4 p4K_Drest, p4l_Drest, p4nu_Drest; + double wtME; + + // --- Outer accept-reject on the V-A matrix element --- + do { + wtME = 1.0; + + // --- Inner accept-reject on 3-body phase space --- + double m23, p1Abs, p23Abs, wtPS; + do { + m23 = m23Min + random->Uniform(0, 1) * mDiff; + p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) + * (mD + mK - m23) * (mD - mK + m23)) / mD; + p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) + * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; + wtPS = p1Abs * p23Abs; + } while (wtPS < random->Uniform(0, 1) * wtPSmax); + + // Set up m23 -> lepton + neutrino, isotropic in m23 rest frame + double cosTheta23 = random->Uniform(-1, 1); + double sinTheta23 = std::sin(std::acos(cosTheta23)); + double phi23 = random->Uniform(0, 2 * M_PI); + geom3::Vector3 dir23(sinTheta23 * std::cos(phi23), + sinTheta23 * std::sin(phi23), + cosTheta23); + rk::P4 p4l_m23rest(p23Abs * dir23, ml); + rk::P4 p4nu_m23rest(-p23Abs * dir23, mnu); + + // Set up D -> K + (m23 system), isotropic in D rest frame + double cosTheta1 = random->Uniform(-1, 1); + double sinTheta1 = std::sin(std::acos(cosTheta1)); + double phi1 = random->Uniform(0, 2 * M_PI); + geom3::Vector3 dir1(sinTheta1 * std::cos(phi1), + sinTheta1 * std::sin(phi1), + cosTheta1); + p4K_Drest = rk::P4(p1Abs * dir1, mK); + rk::P4 p4m23_Drest(-p1Abs * dir1, m23); + + // Boost lepton/neutrino from m23 rest frame to D rest frame + rk::Boost boost_m23_to_Drest = p4m23_Drest.labBoost(); + p4l_Drest = p4l_m23rest.boost(boost_m23_to_Drest); + p4nu_Drest = p4nu_m23rest.boost(boost_m23_to_Drest); + + // V-A matrix element weight: + // wtME = m_D * E_lepton * (p_neutrino . p_kaon) in D rest frame + wtME = mD * p4l_Drest.e() * p4nu_Drest.dot(p4K_Drest); + } while (wtME < random->Uniform(0, 1) * wtMEmax); + + // Boost kaon / lepton / neutrino from D rest frame to lab frame + rk::Boost boost_D_to_lab = p4D_lab.labBoost(); + rk::P4 p4K_lab = p4K_Drest.boost(boost_D_to_lab); + rk::P4 p4l_lab = p4l_Drest.boost(boost_D_to_lab); + rk::P4 p4nu_lab = p4nu_Drest.boost(boost_D_to_lab); + + // Write the secondaries + std::vector & secondaries = record.GetSecondaryParticleRecords(); + siren::dataclasses::SecondaryParticleRecord & kpi = secondaries[0]; + siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[1]; + siren::dataclasses::SecondaryParticleRecord & neutrino = secondaries[2]; + + kpi.SetFourMomentum({p4K_lab.e(), p4K_lab.px(), p4K_lab.py(), p4K_lab.pz()}); + kpi.SetMass(p4K_lab.m()); + kpi.SetHelicity(record.primary_helicity); + + lepton.SetFourMomentum({p4l_lab.e(), p4l_lab.px(), p4l_lab.py(), p4l_lab.pz()}); + lepton.SetMass(p4l_lab.m()); + lepton.SetHelicity(record.primary_helicity); + + neutrino.SetFourMomentum({p4nu_lab.e(), p4nu_lab.px(), p4nu_lab.py(), p4nu_lab.pz()}); + neutrino.SetMass(p4nu_lab.m()); + neutrino.SetHelicity(record.primary_helicity); +} + +double CharmMesonDecay3Body::FinalStateProbability(dataclasses::InteractionRecord const & record) const { + double dd = DifferentialDecayWidth(record); + double td = TotalDecayWidthForFinalState(record); + if (dd == 0) return 0.; + else if (td == 0) return 0.; + else return dd/td; +} + +std::vector CharmMesonDecay3Body::DensityVariables() const { + return std::vector{"Q2"}; +} + + + +} // namespace interactions +} // namespace siren + diff --git a/projects/interactions/private/PythiaDISCrossSection.cxx b/projects/interactions/private/PythiaDISCrossSection.cxx new file mode 100644 index 000000000..26d6e98e1 --- /dev/null +++ b/projects/interactions/private/PythiaDISCrossSection.cxx @@ -0,0 +1,607 @@ +#include "SIREN/interactions/PythiaDISCrossSection.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include + +#include "SIREN/interactions/CrossSection.h" +#include "SIREN/dataclasses/InteractionRecord.h" +#include "SIREN/dataclasses/Particle.h" +#include "SIREN/utilities/Random.h" +#include "SIREN/utilities/Constants.h" + +namespace siren { +namespace interactions { + +namespace { +bool kinematicallyAllowed(double x, double y, double E, double M, double m) { + if(x > 1) return false; + if(x < ((m * m) / (2 * M * (E - m)))) return false; + if (x < 1e-6 || y < 1e-6) return false; + double d = 2 * (1 + (M * x) / (2 * E)); + double ad = 1 - m * m * ((1 / (2 * M * E * x)) + (1 / (2 * E * E))); + double term = 1 - ((m * m) / (2 * M * E * x)); + double bd = sqrt(term * term - ((m * m) / (E * E))); + double s = 2 * M * E; + double Q2 = s * x * y; + double Mc = siren::utilities::Constants::D0Mass; + return ((ad - bd) <= d * y and d * y <= (ad + bd)) && (Q2 * (1 - x) / x + pow(M, 2) >= pow(M + Mc, 2)); +} +} // anonymous namespace + +// ── SIRENRndm: bridges SIREN RNG into Pythia ── + +class SIRENRndm : public Pythia8::RndmEngine { +public: + mutable std::shared_ptr rng_; + double flat() override { return rng_->Uniform(0.0, 1.0); } +}; + +// ── Constructors ── + +PythiaDISCrossSection::PythiaDISCrossSection() {} + +PythiaDISCrossSection::~PythiaDISCrossSection() = default; + +PythiaDISCrossSection::PythiaDISCrossSection( + std::string differential_filename, + std::string total_filename, + int interaction_type, + double target_mass, + double minimum_Q2, + std::set primary_types, + std::set target_types, + std::string pythia_data_path, + std::string pdf_set, + std::string units) + : primary_types_(primary_types), + target_types_(target_types), + interaction_type_(interaction_type), + target_mass_(target_mass), + minimum_Q2_(minimum_Q2), + pdf_set_(pdf_set), + pythia_data_path_(pythia_data_path) +{ + LoadFromFile(differential_filename, total_filename); + InitializeSignatures(); + SetUnits(units); +} + +PythiaDISCrossSection::PythiaDISCrossSection( + std::string differential_filename, + std::string total_filename, + int interaction_type, + double target_mass, + double minimum_Q2, + std::vector primary_types, + std::vector target_types, + std::string pythia_data_path, + std::string pdf_set, + std::string units) + : primary_types_(primary_types.begin(), primary_types.end()), + target_types_(target_types.begin(), target_types.end()), + interaction_type_(interaction_type), + target_mass_(target_mass), + minimum_Q2_(minimum_Q2), + pdf_set_(pdf_set), + pythia_data_path_(pythia_data_path) +{ + LoadFromFile(differential_filename, total_filename); + InitializeSignatures(); + SetUnits(units); +} + +// ── File I/O ── + +void PythiaDISCrossSection::SetUnits(std::string units) { + std::transform(units.begin(), units.end(), units.begin(), + [](unsigned char c){ return std::tolower(c); }); + if(units == "cm") { + unit = 1.0; + } else if(units == "m") { + unit = 10000.0; + } else { + throw std::runtime_error("Cross section units not supported!"); + } +} + +void PythiaDISCrossSection::LoadFromFile(std::string dd_crossSectionFile, std::string total_crossSectionFile) { + differential_cross_section_ = photospline::splinetable<>(dd_crossSectionFile.c_str()); + if(differential_cross_section_.get_ndim() != 3 && differential_cross_section_.get_ndim() != 2) + throw std::runtime_error("cross section spline has " + std::to_string(differential_cross_section_.get_ndim()) + + " dimensions, should have either 3 or 2"); + total_cross_section_ = photospline::splinetable<>(total_crossSectionFile.c_str()); + if(total_cross_section_.get_ndim() != 1) + throw std::runtime_error("Total cross section spline has " + std::to_string(total_cross_section_.get_ndim()) + + " dimensions, should have 1"); +} + +void PythiaDISCrossSection::LoadFromMemory(std::vector & differential_data, std::vector & total_data) { + differential_cross_section_.read_fits_mem(differential_data.data(), differential_data.size()); + total_cross_section_.read_fits_mem(total_data.data(), total_data.size()); +} + +void PythiaDISCrossSection::ReadParamsFromSplineTable() { + bool mass_good = differential_cross_section_.read_key("TARGETMASS", target_mass_); + bool int_good = differential_cross_section_.read_key("INTERACTION", interaction_type_); + bool q2_good = differential_cross_section_.read_key("Q2MIN", minimum_Q2_); + if(!int_good) interaction_type_ = 1; + if(!q2_good) minimum_Q2_ = 1; + if(!mass_good) target_mass_ = siren::utilities::Constants::isoscalarMass; +} + +// ── Equality ── + +bool PythiaDISCrossSection::equal(CrossSection const & other) const { + const PythiaDISCrossSection* x = dynamic_cast(&other); + if(!x) return false; + return std::tie(interaction_type_, target_mass_, minimum_Q2_, signatures_, primary_types_, target_types_) + == std::tie(x->interaction_type_, x->target_mass_, x->minimum_Q2_, x->signatures_, x->primary_types_, x->target_types_); +} + +// ── Particle ID helpers ── + +bool PythiaDISCrossSection::IsCharmedHadron(int pdgId) { + int abs_id = std::abs(pdgId); + // Only match D0 (421) and D+/- (411) for now. + // TODO: Add Ds (431) and Lambda_c (4122) support — need CharmMesonDecay + // to handle these types before they can be registered as signatures. + // Currently Ds/Lambda_c end up in the hadronic remnant. + return (abs_id == 411 || abs_id == 421); +} + +siren::dataclasses::ParticleType PythiaDISCrossSection::PdgToParticleType(int pdgId) { + // Direct cast — SIREN ParticleType enum values match PDG codes + return static_cast(pdgId); +} + +double PythiaDISCrossSection::GetLeptonMass(siren::dataclasses::ParticleType lepton_type) { + int32_t lepton_number = std::abs(static_cast(lepton_type)); + switch(lepton_number) { + case 11: return siren::utilities::Constants::electronMass; + case 13: return siren::utilities::Constants::muonMass; + case 15: return siren::utilities::Constants::tauMass; + case 12: case 14: case 16: return 0; + default: throw std::runtime_error("Unknown lepton type!"); + } +} + +double PythiaDISCrossSection::GetHadronMass(siren::dataclasses::ParticleType hadron_type) { + switch(hadron_type) { + case siren::dataclasses::ParticleType::D0: + case siren::dataclasses::ParticleType::D0Bar: + return siren::utilities::Constants::D0Mass; + case siren::dataclasses::ParticleType::DPlus: + case siren::dataclasses::ParticleType::DMinus: + return siren::utilities::Constants::DPlusMass; + default: + return 0.0; + } +} + +std::map PythiaDISCrossSection::getIndices(siren::dataclasses::InteractionSignature signature) { + int lepton_id = -1, hadron_id = -1, meson_id = -1; + for (size_t i = 0; i < signature.secondary_types.size(); i++) { + if (siren::dataclasses::isLepton(signature.secondary_types[i])) { + lepton_id = i; + } else if (siren::dataclasses::isD(signature.secondary_types[i])) { + meson_id = i; + } else { + hadron_id = i; + } + } + return {{"lepton", lepton_id}, {"hadron", hadron_id}, {"meson", meson_id}}; +} + +// ── Signatures ── + +void PythiaDISCrossSection::InitializeSignatures() { + signatures_.clear(); + for(auto primary_type : primary_types_) { + dataclasses::InteractionSignature signature; + signature.primary_type = primary_type; + if(not siren::dataclasses::isNeutrino(primary_type)) { + throw std::runtime_error("PythiaDISCrossSection only supports neutrinos as primaries!"); + } + + siren::dataclasses::ParticleType charged_lepton_product = siren::dataclasses::ParticleType::unknown; + siren::dataclasses::ParticleType neutral_lepton_product = primary_type; + + if(primary_type == siren::dataclasses::ParticleType::NuE) { + charged_lepton_product = siren::dataclasses::ParticleType::EMinus; + } else if(primary_type == siren::dataclasses::ParticleType::NuEBar) { + charged_lepton_product = siren::dataclasses::ParticleType::EPlus; + } else if(primary_type == siren::dataclasses::ParticleType::NuMu) { + charged_lepton_product = siren::dataclasses::ParticleType::MuMinus; + } else if(primary_type == siren::dataclasses::ParticleType::NuMuBar) { + charged_lepton_product = siren::dataclasses::ParticleType::MuPlus; + } else if(primary_type == siren::dataclasses::ParticleType::NuTau) { + charged_lepton_product = siren::dataclasses::ParticleType::TauMinus; + } else if(primary_type == siren::dataclasses::ParticleType::NuTauBar) { + charged_lepton_product = siren::dataclasses::ParticleType::TauPlus; + } else { + throw std::runtime_error("InitializeSignatures: Unknown parent neutrino type!"); + } + + if(interaction_type_ == 1) { + signature.secondary_types.push_back(charged_lepton_product); + } else if(interaction_type_ == 2) { + signature.secondary_types.push_back(neutral_lepton_product); + } else { + throw std::runtime_error("InitializeSignatures: Unknown interaction type!"); + } + + // Hadron remnant + signature.secondary_types.push_back(siren::dataclasses::ParticleType::Hadrons); + + // Charmed meson types: always D0 and DPlus regardless of nu/nubar. + // This matches the existing CharmHadronization convention and ensures + // compatibility with CharmMesonDecay (which only supports D0/DPlus). + // TODO: Add Ds (431) and Lambda_c (4122) support. + D_types_ = {siren::dataclasses::ParticleType::D0, + siren::dataclasses::ParticleType::DPlus}; + + for (auto meson_type : D_types_) { + dataclasses::InteractionSignature full_signature = signature; + full_signature.secondary_types.push_back(meson_type); + for(auto target_type : target_types_) { + full_signature.target_type = target_type; + signatures_.push_back(full_signature); + std::pair key(primary_type, target_type); + signatures_by_parent_types_[key].push_back(full_signature); + } + } + } +} + +// ── Cross sections (from splines, same as QuarkDISFromSpline) ── + +double PythiaDISCrossSection::TotalCrossSection(dataclasses::InteractionRecord const & interaction) const { + siren::dataclasses::ParticleType primary_type = interaction.signature.primary_type; + double primary_energy = interaction.primary_momentum[0]; + if(primary_energy < InteractionThreshold(interaction)) return 0; + return TotalCrossSection(primary_type, primary_energy); +} + +double PythiaDISCrossSection::TotalCrossSection(siren::dataclasses::ParticleType primary_type, double primary_energy) const { + if(not primary_types_.count(primary_type)) { + throw std::runtime_error("Supplied primary not supported by cross section!"); + } + double log_energy = log10(primary_energy); + if(log_energy < total_cross_section_.lower_extent(0) + or log_energy > total_cross_section_.upper_extent(0)) { + throw std::runtime_error("Interaction energy out of cross section table range"); + } + int center; + total_cross_section_.searchcenters(&log_energy, ¢er); + double log_xs = total_cross_section_.ndsplineeval(&log_energy, ¢er, 0); + return unit * std::pow(10.0, log_xs); +} + +double PythiaDISCrossSection::TotalCrossSectionAllFinalStates(dataclasses::InteractionRecord const & record) const { + return TotalCrossSection(record); +} + +double PythiaDISCrossSection::DifferentialCrossSection(dataclasses::InteractionRecord const & interaction) const { + rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); + rk::P4 p2(geom3::Vector3(0, 0, 0), interaction.target_mass); + double primary_energy = interaction.primary_momentum[0]; + + std::map secondaries = getIndices(interaction.signature); + unsigned int lepton_index = secondaries["lepton"]; + + std::array const & mom3 = interaction.secondary_momenta[lepton_index]; + rk::P4 p3(geom3::Vector3(mom3[1], mom3[2], mom3[3]), interaction.secondary_masses[lepton_index]); + rk::P4 q = p1 - p3; + + double Q2 = -q.dot(q); + double lepton_mass = GetLeptonMass(interaction.signature.secondary_types[lepton_index]); + double y = 1.0 - p2.dot(p3) / p2.dot(p1); + double x = Q2 / (2.0 * p2.dot(q)); + + double log_energy = log10(primary_energy); + std::array coordinates{{log_energy, log10(x), log10(y)}}; + std::array centers; + + if (Q2 < minimum_Q2_ || !kinematicallyAllowed(x, y, primary_energy, target_mass_, lepton_mass) + || !differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { + // Fall back to saved parameters + x = interaction.interaction_parameters.at("bjorken_x"); + y = interaction.interaction_parameters.at("bjorken_y"); + Q2 = 2. * primary_energy * p2.e() * x * y; + } + return DifferentialCrossSection(primary_energy, x, y, lepton_mass, Q2); +} + +double PythiaDISCrossSection::DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2) const { + double log_energy = log10(energy); + if(log_energy < differential_cross_section_.lower_extent(0) + || log_energy > differential_cross_section_.upper_extent(0)) + return 0.0; + if(x <= 0 || x >= 1) return 0.0; + if(y <= 0 || y >= 1) return 0.0; + if(std::isnan(Q2)) Q2 = 2.0 * energy * target_mass_ * x * y; + if(Q2 < minimum_Q2_) return 0; + if(!kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) return 0; + + std::array coordinates{{log_energy, log10(x), log10(y)}}; + std::array centers; + if(!differential_cross_section_.searchcenters(coordinates.data(), centers.data())) return 0; + double result = pow(10., differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0)); + return unit * result; +} + +double PythiaDISCrossSection::InteractionThreshold(dataclasses::InteractionRecord const & interaction) const { + return 0; +} + +// ── Fragmentation fractions ── + +double PythiaDISCrossSection::FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const { + // Approximate fractions from Pythia (charm hadronization) + if (secondary == siren::dataclasses::ParticleType::D0 || secondary == siren::dataclasses::ParticleType::D0Bar) { + return 0.6; + } else if (secondary == siren::dataclasses::ParticleType::DPlus || secondary == siren::dataclasses::ParticleType::DMinus) { + return 0.23; + } + // TODO: Add Ds (~0.08) and Lambda_c (~0.09) when signatures include them + return 0; +} + +double PythiaDISCrossSection::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { + // Pythia samples final-state kinematics from the physical distribution, + // so no differential reweighting is needed. FinalStateProbability appears + // in both physical_probability and generation_probability and cancels. + return 1.0; +} + +// ── Signature accessors ── + +std::vector PythiaDISCrossSection::GetPossiblePrimaries() const { + return std::vector(primary_types_.begin(), primary_types_.end()); +} + +std::vector PythiaDISCrossSection::GetPossibleTargetsFromPrimary(siren::dataclasses::ParticleType primary_type) const { + return std::vector(target_types_.begin(), target_types_.end()); +} + +std::vector PythiaDISCrossSection::GetPossibleSignatures() const { + return std::vector(signatures_.begin(), signatures_.end()); +} + +std::vector PythiaDISCrossSection::GetPossibleTargets() const { + return std::vector(target_types_.begin(), target_types_.end()); +} + +std::vector PythiaDISCrossSection::GetPossibleSignaturesFromParents(siren::dataclasses::ParticleType primary_type, siren::dataclasses::ParticleType target_type) const { + std::pair key(primary_type, target_type); + if(signatures_by_parent_types_.find(key) != signatures_by_parent_types_.end()) { + return signatures_by_parent_types_.at(key); + } + return std::vector(); +} + +std::vector PythiaDISCrossSection::DensityVariables() const { + return std::vector{"Bjorken x", "Bjorken y"}; +} + +// ══════════════════════════════════════════════════════════════════════ +// Pythia initialization and SampleFinalState — the core new logic +// ══════════════════════════════════════════════════════════════════════ + +void PythiaDISCrossSection::InitializePythia(double E_nu) const { + // Ensure LHAPDF can find PDF sets — derive data path from the LHAPDF library location + // The pdf_set_ is e.g. "LHAPDF6:HERAPDF20_NLO_EIG", and LHAPDF needs LHAPDF_DATA_PATH set + const char* lhapdf_path = std::getenv("LHAPDF_DATA_PATH"); + if (!lhapdf_path) { + // Try a reasonable default based on common install layouts + std::string default_path = "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/pzhelnin/LHAPDF/new_install/share/LHAPDF"; + setenv("LHAPDF_DATA_PATH", default_path.c_str(), 0); + } + + pythia_ = std::make_unique(pythia_data_path_, false); + + // Determine beam particle from primary types + int beam_id = 14; // default nu_mu + for (auto pt : primary_types_) { + beam_id = static_cast(pt); + break; // use the first primary type + } + + // Process selection + if (interaction_type_ == 1) { + pythia_->readString("WeakBosonExchange:ff2ff(t:W) = on"); + } else if (interaction_type_ == 2) { + pythia_->readString("WeakBosonExchange:ff2ff(t:gmZ) = on"); + } + + // Beam setup: fixed target + pythia_->readString("Beams:frameType = 2"); + pythia_->readString("Beams:idA = " + std::to_string(beam_id)); + pythia_->readString("Beams:idB = 2212"); + pythia_->readString("Beams:eA = " + std::to_string(E_nu)); + pythia_->readString("Beams:eB = 0."); + + // Force charm-only for CC: zero out non-charm CKM elements + // Must use forceParm to bypass Pythia's range checks + if (interaction_type_ == 1) { + pythia_->settings.forceParm("StandardModel:Vud", 0.0); + pythia_->settings.forceParm("StandardModel:Vus", 0.0); + pythia_->settings.forceParm("StandardModel:Vub", 0.0); + pythia_->settings.forceParm("StandardModel:Vcb", 0.0); + // Keeps Vcd ~ 0.225 and Vcs ~ 0.973 → every CC event produces charm + } + + // PDF + pythia_->readString("PDF:pSet = " + pdf_set_); + pythia_->readString("PDF:useHard = on"); + pythia_->readString("PDF:pHardSet = " + pdf_set_); + + // Hadronization: string fragmentation ON, D decays OFF + pythia_->readString("HadronLevel:Hadronize = on"); + pythia_->readString("HadronLevel:Decay = off"); + + // Disable MPI + pythia_->readString("PartonLevel:MPI = off"); + + // Phase space: remove mHatMin (default 4 GeV) to allow full kinematic range. + // Keep pTHatMinDiverge at default (1 GeV), giving effective Q2 > 1 GeV^2. + pythia_->readString("PhaseSpace:mHatMin = 0.0"); + pythia_->readString("PhaseSpace:Q2Min = " + std::to_string(minimum_Q2_)); + + pythia_->readString("Print:quiet = on"); + + if (!pythia_->init()) { + throw std::runtime_error("PythiaDISCrossSection: Pythia initialization failed"); + } + + // Bridge SIREN RNG into Pythia for reproducibility. + // Must be done after init() — init uses Pythia's internal RNG for setup. + // The SIREN RNG is connected here and updated per-event in SampleFinalState. + siren_rndm_ = std::make_shared(); + pythia_->rndm.rndmEnginePtr(siren_rndm_); + + pythia_initialized_ = true; +} + +void PythiaDISCrossSection::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { + // Get indices for lepton, hadron, meson in the secondary list + std::map secondary_indices = getIndices(record.signature); + unsigned int lepton_index = secondary_indices["lepton"]; + unsigned int hadron_index = secondary_indices["hadron"]; + unsigned int meson_index = secondary_indices["meson"]; + + // Get primary neutrino 4-momentum + rk::P4 p1(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); + double E_nu = p1.e(); + geom3::UnitVector3 nu_dir = p1.momentum().direction(); + + // Initialize or update Pythia + if (!pythia_initialized_) { + InitializePythia(E_nu); + } + + // For now, reinitialize if energy changes significantly + // TODO: test setKinematics stability across energy ranges + + // Bridge the SIREN RNG for this event + siren_rndm_->rng_ = random; + + // Generate a Pythia event + int max_attempts = 100; + bool found_charm = false; + for (int attempt = 0; attempt < max_attempts; ++attempt) { + if (!pythia_->next()) { + continue; + } + + // Find the muon and charmed hadron in the final state + int i_muon = -1; + int i_charm = -1; + Pythia8::Vec4 p_remnant(0., 0., 0., 0.); + + for (int i = 0; i < pythia_->event.size(); ++i) { + if (!pythia_->event[i].isFinal()) continue; + + int pid = pythia_->event[i].id(); + int abs_pid = std::abs(pid); + + if (abs_pid == 13 && i_muon < 0) { + // Primary muon (first one found) + i_muon = i; + } else if (IsCharmedHadron(pid) && i_charm < 0) { + // First charmed hadron + i_charm = i; + } else { + // Everything else goes into the remnant + p_remnant += pythia_->event[i].p(); + } + } + + if (i_muon >= 0 && i_charm >= 0) { + found_charm = true; + + // Extract 4-momenta from Pythia (in Pythia's frame: nu along +z) + Pythia8::Vec4 p_mu_pythia = pythia_->event[i_muon].p(); + Pythia8::Vec4 p_D_pythia = pythia_->event[i_charm].p(); + + // Get Pythia's DIS kinematics for weighting + double pythia_x = pythia_->info.x2(); // proton PDF x (beam B) + double pythia_y = 1.0 - p_mu_pythia.e() / E_nu; + + // Store in interaction parameters for weighting + record.interaction_parameters.clear(); + record.interaction_parameters["energy"] = E_nu; + record.interaction_parameters["bjorken_x"] = pythia_x; + record.interaction_parameters["bjorken_y"] = pythia_y; + + // Rotate from Pythia frame (+z) to SIREN's neutrino direction + geom3::UnitVector3 z_dir = geom3::UnitVector3::zAxis(); + geom3::Rotation3 rot = geom3::rotationBetween(z_dir, nu_dir); + + // Helper lambda to convert Pythia Vec4 → rotated rk::P4 + auto pythia_to_siren = [&](const Pythia8::Vec4 & pv, double mass) -> rk::P4 { + geom3::Vector3 mom(pv.px(), pv.py(), pv.pz()); + mom = rot * mom; + return rk::P4(mom, mass); + }; + + double muon_mass = pythia_->event[i_muon].m(); + double D_mass = pythia_->event[i_charm].m(); + double remnant_mass = std::sqrt(std::max(0.0, + p_remnant.e() * p_remnant.e() - + p_remnant.px() * p_remnant.px() - + p_remnant.py() * p_remnant.py() - + p_remnant.pz() * p_remnant.pz())); + + rk::P4 p3 = pythia_to_siren(p_mu_pythia, muon_mass); + rk::P4 p_D = pythia_to_siren(p_D_pythia, D_mass); + + // Remnant + geom3::Vector3 rem_mom(p_remnant.px(), p_remnant.py(), p_remnant.pz()); + rem_mom = rot * rem_mom; + rk::P4 p_rem(rem_mom, remnant_mass); + + // Set secondaries + std::vector & secondaries = record.GetSecondaryParticleRecords(); + siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[lepton_index]; + siren::dataclasses::SecondaryParticleRecord & hadron = secondaries[hadron_index]; + siren::dataclasses::SecondaryParticleRecord & meson = secondaries[meson_index]; + + lepton.SetFourMomentum({p3.e(), p3.px(), p3.py(), p3.pz()}); + lepton.SetMass(muon_mass); + lepton.SetHelicity(record.primary_helicity); + + hadron.SetFourMomentum({p_rem.e(), p_rem.px(), p_rem.py(), p_rem.pz()}); + hadron.SetMass(remnant_mass); + hadron.SetHelicity(record.target_helicity); + + meson.SetFourMomentum({p_D.e(), p_D.px(), p_D.py(), p_D.pz()}); + meson.SetMass(D_mass); + meson.SetHelicity(record.target_helicity); + + break; + } + } + + if (!found_charm) { + throw std::runtime_error("PythiaDISCrossSection::SampleFinalState: Failed to generate charm event after " + std::to_string(max_attempts) + " attempts"); + } +} + +} // namespace interactions +} // namespace siren diff --git a/projects/interactions/private/pybindings/CharmMesonDecay3Body.h b/projects/interactions/private/pybindings/CharmMesonDecay3Body.h new file mode 100644 index 000000000..560251168 --- /dev/null +++ b/projects/interactions/private/pybindings/CharmMesonDecay3Body.h @@ -0,0 +1,34 @@ +#include +#include +#include + +#include +#include +#include + +#include "../../public/SIREN/interactions/CrossSection.h" +#include "../../public/SIREN/interactions/Decay.h" +#include "../../public/SIREN/interactions/CharmMesonDecay3Body.h" + +#include "../../public/SIREN/interactions/pyDecay.h" +#include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" +#include "../../../geometry/public/SIREN/geometry/Geometry.h" +#include "../../../utilities/public/SIREN/utilities/Random.h" + +void register_CharmMesonDecay3Body(pybind11::module_ & m) { + using namespace pybind11; + using namespace siren::interactions; + + class_, Decay> charmmesondecay3body(m, "CharmMesonDecay3Body"); + + charmmesondecay3body + + .def(init<>()) + .def(init(), + arg("primary_type")) + .def(self == self) + .def("SampleFinalState",&CharmMesonDecay3Body::SampleFinalState) + .def("GetPossibleSignatures",&CharmMesonDecay3Body::GetPossibleSignatures) + .def("GetPossibleSignaturesFromParent",&CharmMesonDecay3Body::GetPossibleSignaturesFromParent); + +} diff --git a/projects/interactions/private/pybindings/PythiaDISCrossSection.h b/projects/interactions/private/pybindings/PythiaDISCrossSection.h new file mode 100644 index 000000000..44a79e3fe --- /dev/null +++ b/projects/interactions/private/pybindings/PythiaDISCrossSection.h @@ -0,0 +1,70 @@ +#include +#include +#include + +#include +#include +#include + +#include "../../public/SIREN/interactions/CrossSection.h" +#include "../../public/SIREN/interactions/PythiaDISCrossSection.h" +#include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" +#include "../../../dataclasses/public/SIREN/dataclasses/InteractionRecord.h" +#include "../../../dataclasses/public/SIREN/dataclasses/InteractionSignature.h" +#include "../../../geometry/public/SIREN/geometry/Geometry.h" +#include "../../../utilities/public/SIREN/utilities/Random.h" + +void register_PythiaDISCrossSection(pybind11::module_ & m) { + using namespace pybind11; + using namespace siren::interactions; + + class_, CrossSection> pythiadis(m, "PythiaDISCrossSection"); + + pythiadis + + .def(init<>()) + .def(init, + std::set, + std::string, std::string, std::string>(), + arg("differential_filename"), + arg("total_filename"), + arg("interaction_type"), + arg("target_mass"), + arg("minimum_Q2"), + arg("primary_types"), + arg("target_types"), + arg("pythia_data_path"), + arg("pdf_set") = std::string("LHAPDF6:HERAPDF20_NLO_EIG"), + arg("units") = std::string("cm")) + .def(init, + std::vector, + std::string, std::string, std::string>(), + arg("differential_filename"), + arg("total_filename"), + arg("interaction_type"), + arg("target_mass"), + arg("minimum_Q2"), + arg("primary_types"), + arg("target_types"), + arg("pythia_data_path"), + arg("pdf_set") = std::string("LHAPDF6:HERAPDF20_NLO_EIG"), + arg("units") = std::string("cm")) + .def(self == self) + .def("TotalCrossSection",overload_cast(&PythiaDISCrossSection::TotalCrossSection, const_)) + .def("TotalCrossSection",overload_cast(&PythiaDISCrossSection::TotalCrossSection, const_)) + .def("DifferentialCrossSection",overload_cast(&PythiaDISCrossSection::DifferentialCrossSection, const_)) + .def("DifferentialCrossSection",overload_cast(&PythiaDISCrossSection::DifferentialCrossSection, const_)) + .def("InteractionThreshold",&PythiaDISCrossSection::InteractionThreshold) + .def("FragmentationFraction",&PythiaDISCrossSection::FragmentationFraction) + .def("GetPossibleTargets",&PythiaDISCrossSection::GetPossibleTargets) + .def("GetPossibleTargetsFromPrimary",&PythiaDISCrossSection::GetPossibleTargetsFromPrimary) + .def("GetPossiblePrimaries",&PythiaDISCrossSection::GetPossiblePrimaries) + .def("GetPossibleSignatures",&PythiaDISCrossSection::GetPossibleSignatures) + .def("GetPossibleSignaturesFromParents",&PythiaDISCrossSection::GetPossibleSignaturesFromParents) + .def("FinalStateProbability",&PythiaDISCrossSection::FinalStateProbability) + .def("GetMinimumQ2",&PythiaDISCrossSection::GetMinimumQ2) + .def("GetTargetMass",&PythiaDISCrossSection::GetTargetMass) + .def("GetInteractionType",&PythiaDISCrossSection::GetInteractionType); +} diff --git a/projects/interactions/private/pybindings/interactions.cxx b/projects/interactions/private/pybindings/interactions.cxx index 9a2c628e2..569645775 100644 --- a/projects/interactions/private/pybindings/interactions.cxx +++ b/projects/interactions/private/pybindings/interactions.cxx @@ -14,7 +14,9 @@ #include "../../public/SIREN/interactions/Hadronization.h" #include "../../public/SIREN/interactions/CharmHadronization.h" #include "../../public/SIREN/interactions/CharmMesonDecay.h" +#include "../../public/SIREN/interactions/CharmMesonDecay3Body.h" #include "../../public/SIREN/interactions/DMesonELoss.h" +#include "../../public/SIREN/interactions/PythiaDISCrossSection.h" #include "./CrossSection.h" #include "./DipoleFromTable.h" @@ -31,7 +33,9 @@ #include "./Hadronization.h" #include "./CharmHadronization.h" #include "./CharmMesonDecay.h" +#include "./CharmMesonDecay3Body.h" #include "./DMesonELoss.h" +#include "./PythiaDISCrossSection.h" #include #include @@ -50,6 +54,7 @@ PYBIND11_MODULE(interactions,m) { register_CharmHadronization(m); register_CharmMesonDecay(m); + register_CharmMesonDecay3Body(m); register_DMesonELoss(m); register_DipoleFromTable(m); @@ -62,4 +67,5 @@ PYBIND11_MODULE(interactions,m) { register_NeutrissimoDecay(m); register_InteractionCollection(m); register_DummyCrossSection(m); + register_PythiaDISCrossSection(m); } diff --git a/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h b/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h new file mode 100644 index 000000000..5f0c4e1e8 --- /dev/null +++ b/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h @@ -0,0 +1,101 @@ +#pragma once +#ifndef SIREN_CharmMesonDecay3Body_H +#define SIREN_CharmMesonDecay3Body_H + +// CharmMesonDecay3Body — Pythia-style 3-body phase-space decay for D mesons +// +// Sister class to CharmMesonDecay (the legacy 2-body-cascade implementation). +// Both inherit from Decay and share the same decay-width machinery +// (DifferentialDecayWidth, TotalDecayWidthForFinalState, FinalStateProbability, +// and the signature catalog). The only behavioural difference is in +// SampleFinalState, where this class generates the final-state kinematics by +// sampling 3-body phase space (following Pythia's ParticleDecays::threeBody) +// with V-A matrix element reweighting. It also mixes D -> K l nu with +// D -> K*(892) l nu channels per event, in line with PDG branching ratios. + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "SIREN/dataclasses/Particle.h" +#include "SIREN/dataclasses/InteractionSignature.h" +#include "SIREN/dataclasses/InteractionRecord.h" + +#include "SIREN/interactions/Decay.h" +#include "SIREN/utilities/Interpolator.h" + + +namespace siren { +namespace interactions { + +class CharmMesonDecay3Body : public Decay { +friend cereal::access; +private: + const std::set primary_types = {siren::dataclasses::Particle::ParticleType::D0, siren::dataclasses::Particle::ParticleType::DPlus}; + siren::utilities::Interpolator1D inverseCdf; // for dGamma (used in FinalStateProbability) +public: + CharmMesonDecay3Body(); + CharmMesonDecay3Body(siren::dataclasses::Particle::ParticleType primary); + virtual bool equal(Decay const & other) const override; + static double particleMass(siren::dataclasses::ParticleType particle); + double TotalDecayWidth(dataclasses::InteractionRecord const &) const override; + double TotalDecayWidth(siren::dataclasses::Particle::ParticleType primary) const override; + double TotalDecayWidthForFinalState(dataclasses::InteractionRecord const &) const override; + double DifferentialDecayWidth(dataclasses::InteractionRecord const &) const override; + double DifferentialDecayWidth(std::vector constants, double Q2, double mD, double mK) const; + void SampleFinalStateHadronic(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr) const; + void SampleFinalState(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr) const override; + std::vector GetPossibleSignatures() const override; + std::vector GetPossibleSignaturesFromParent(siren::dataclasses::Particle::ParticleType primary) const override; + virtual double FinalStateProbability(dataclasses::InteractionRecord const & record) const override; + std::vector FormFactorFromRecord(dataclasses::CrossSectionDistributionRecord const & record) const; + void computeDiffGammaCDF(std::vector constants, double mD, double mK); + +public: + virtual std::vector DensityVariables() const override; + template + void save(Archive & archive, std::uint32_t const version) const { + if(version == 0) { + archive(::cereal::make_nvp("PrimaryTypes", primary_types)); + archive(::cereal::make_nvp("Decay", cereal::virtual_base_class(this))); + } else { + throw std::runtime_error("CharmMesonDecay3Body only supports version <= 0!"); + } + } + template + void load_and_construct(Archive & archive, cereal::construct & construct, std::uint32_t version) { + if(version == 0) { + std::set _primary_types; + + archive(::cereal::make_nvp("PrimaryTypes", _primary_types)); + construct(_primary_types); + archive(::cereal::make_nvp("Decay", cereal::virtual_base_class(construct.ptr()))); + } else { + throw std::runtime_error("CharmMesonDecay3Body only supports version <= 0!"); + } + } + +}; + +} // namespace interactions +} // namespace siren + +CEREAL_CLASS_VERSION(siren::interactions::CharmMesonDecay3Body, 0); +CEREAL_REGISTER_TYPE(siren::interactions::CharmMesonDecay3Body); +CEREAL_REGISTER_POLYMORPHIC_RELATION(siren::interactions::Decay, siren::interactions::CharmMesonDecay3Body); + +#endif // SIREN_CharmMesonDecay3Body_H diff --git a/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h b/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h new file mode 100644 index 000000000..4b967ea14 --- /dev/null +++ b/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h @@ -0,0 +1,221 @@ +#pragma once +#ifndef SIREN_PythiaDISCrossSection_H +#define SIREN_PythiaDISCrossSection_H + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "SIREN/interactions/CrossSection.h" +#include "SIREN/dataclasses/InteractionSignature.h" +#include "SIREN/dataclasses/Particle.h" + +// Forward declarations for Pythia +namespace Pythia8 { + class Pythia; +} + +namespace siren { namespace dataclasses { class InteractionRecord; } } +namespace siren { namespace utilities { class SIREN_random; } } + +namespace siren { +namespace interactions { + +// Forward declaration — defined in .cxx +class SIRENRndm; + +class PythiaDISCrossSection : public CrossSection { +friend cereal::access; +private: + // Splines for total/differential cross section (SIREN weighting) + photospline::splinetable<> differential_cross_section_; + photospline::splinetable<> total_cross_section_; + + // Pythia instance (mutable because SampleFinalState is const) + mutable std::unique_ptr pythia_; + mutable bool pythia_initialized_ = false; + mutable std::shared_ptr siren_rndm_; + + // Signature bookkeeping + std::vector signatures_; + std::set primary_types_; + std::set target_types_; + std::map> targets_by_primary_types_; + std::map, std::vector> signatures_by_parent_types_; + std::set D_types_; + + // DIS parameters + int interaction_type_; // 1=CC, 2=NC + double target_mass_; + double minimum_Q2_; + double unit; + + // Pythia configuration + std::string pdf_set_; + std::string pythia_data_path_; + + // Helper methods + void InitializePythia(double E_nu) const; + void InitializeSignatures(); + void LoadFromFile(std::string differential_filename, std::string total_filename); + void LoadFromMemory(std::vector & differential_data, std::vector & total_data); + void ReadParamsFromSplineTable(); + void SetUnits(std::string units); + + // Particle ID helpers + static bool IsCharmedHadron(int pdgId); + static siren::dataclasses::ParticleType PdgToParticleType(int pdgId); + static double GetLeptonMass(siren::dataclasses::ParticleType lepton_type); + static double GetHadronMass(siren::dataclasses::ParticleType hadron_type); + static std::map getIndices(siren::dataclasses::InteractionSignature signature); + +public: + PythiaDISCrossSection(); + ~PythiaDISCrossSection(); + + // Main constructor + PythiaDISCrossSection( + std::string differential_filename, + std::string total_filename, + int interaction_type, + double target_mass, + double minimum_Q2, + std::set primary_types, + std::set target_types, + std::string pythia_data_path, + std::string pdf_set = "LHAPDF6:HERAPDF20_NLO_EIG", + std::string units = "cm" + ); + + // Constructor with vectors + PythiaDISCrossSection( + std::string differential_filename, + std::string total_filename, + int interaction_type, + double target_mass, + double minimum_Q2, + std::vector primary_types, + std::vector target_types, + std::string pythia_data_path, + std::string pdf_set = "LHAPDF6:HERAPDF20_NLO_EIG", + std::string units = "cm" + ); + + virtual bool equal(CrossSection const & other) const override; + + // Cross section from splines + double TotalCrossSection(dataclasses::InteractionRecord const &) const override; + double TotalCrossSection(siren::dataclasses::ParticleType primary, double energy) const; + double TotalCrossSectionAllFinalStates(dataclasses::InteractionRecord const &) const override; + double DifferentialCrossSection(dataclasses::InteractionRecord const &) const override; + double DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2=std::numeric_limits::quiet_NaN()) const; + double InteractionThreshold(dataclasses::InteractionRecord const &) const override; + + // Fragmentation fraction (from Pythia output statistics) + double FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const; + + // Final state sampling via Pythia + void SampleFinalState(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr random) const override; + + // Signature methods + std::vector GetPossibleTargets() const override; + std::vector GetPossibleTargetsFromPrimary(siren::dataclasses::ParticleType primary_type) const override; + std::vector GetPossiblePrimaries() const override; + std::vector GetPossibleSignatures() const override; + std::vector GetPossibleSignaturesFromParents(siren::dataclasses::ParticleType primary_type, siren::dataclasses::ParticleType target_type) const override; + virtual double FinalStateProbability(dataclasses::InteractionRecord const & record) const override; + virtual std::vector DensityVariables() const override; + + // Getters + double GetMinimumQ2() const { return minimum_Q2_; } + double GetTargetMass() const { return target_mass_; } + int GetInteractionType() const { return interaction_type_; } + +public: + template + void save(Archive & archive, std::uint32_t const version) const { + if(version == 0) { + splinetable_buffer buf; + buf.size = 0; + auto result_obj = differential_cross_section_.write_fits_mem(); + buf.data = result_obj.first; + buf.size = result_obj.second; + + std::vector diff_blob; + diff_blob.resize(buf.size); + std::copy((char*)buf.data, (char*)buf.data + buf.size, &diff_blob[0]); + + archive(::cereal::make_nvp("DifferentialCrossSectionSpline", diff_blob)); + + buf.size = 0; + result_obj = total_cross_section_.write_fits_mem(); + buf.data = result_obj.first; + buf.size = result_obj.second; + + std::vector total_blob; + total_blob.resize(buf.size); + std::copy((char*)buf.data, (char*)buf.data + buf.size, &total_blob[0]); + + archive(::cereal::make_nvp("TotalCrossSectionSpline", total_blob)); + archive(::cereal::make_nvp("PrimaryTypes", primary_types_)); + archive(::cereal::make_nvp("TargetTypes", target_types_)); + archive(::cereal::make_nvp("InteractionType", interaction_type_)); + archive(::cereal::make_nvp("TargetMass", target_mass_)); + archive(::cereal::make_nvp("MinimumQ2", minimum_Q2_)); + archive(::cereal::make_nvp("Unit", unit)); + archive(::cereal::make_nvp("PdfSet", pdf_set_)); + archive(::cereal::make_nvp("PythiaDataPath", pythia_data_path_)); + archive(cereal::virtual_base_class(this)); + } else { + throw std::runtime_error("PythiaDISCrossSection only supports version <= 0!"); + } + } + template + void load(Archive & archive, std::uint32_t version) { + if(version == 0) { + std::vector differential_data; + std::vector total_data; + archive(::cereal::make_nvp("DifferentialCrossSectionSpline", differential_data)); + archive(::cereal::make_nvp("TotalCrossSectionSpline", total_data)); + archive(::cereal::make_nvp("PrimaryTypes", primary_types_)); + archive(::cereal::make_nvp("TargetTypes", target_types_)); + archive(::cereal::make_nvp("InteractionType", interaction_type_)); + archive(::cereal::make_nvp("TargetMass", target_mass_)); + archive(::cereal::make_nvp("MinimumQ2", minimum_Q2_)); + archive(::cereal::make_nvp("Unit", unit)); + archive(::cereal::make_nvp("PdfSet", pdf_set_)); + archive(::cereal::make_nvp("PythiaDataPath", pythia_data_path_)); + archive(cereal::virtual_base_class(this)); + LoadFromMemory(differential_data, total_data); + InitializeSignatures(); + } else { + throw std::runtime_error("PythiaDISCrossSection only supports version <= 0!"); + } + } +}; + +} // namespace interactions +} // namespace siren + +CEREAL_CLASS_VERSION(siren::interactions::PythiaDISCrossSection, 0); +CEREAL_REGISTER_TYPE(siren::interactions::PythiaDISCrossSection); +CEREAL_REGISTER_POLYMORPHIC_RELATION(siren::interactions::CrossSection, siren::interactions::PythiaDISCrossSection); + +#endif // SIREN_PythiaDISCrossSection_H From 51fd813fbd86458f032b89e6da3eea594b1cf75f Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Tue, 14 Apr 2026 12:45:36 -0400 Subject: [PATCH 24/93] Patch vendored photospline to restore missing bspline symbols Upstream photospline @ c6fb3ea (the pin used by this SIREN version) declares bsplvb_simple() and bspline_deriv_nonzero() in the public header but does not compile implementations for them. The inline template code in detail/bspline_eval.h calls them, which causes undefined-symbol link errors when libSIREN.so is loaded at runtime. Add cmake/photospline_patches/ + patch-apply logic in cmake/Packages/photospline.cmake that restores the implementations from an earlier photospline revision (cee6138 bspline.cpp lines 69-222). Patch is applied idempotently via patch(1) --forward before add_subdirectory(vendor/photospline), so clones of dev/DMeson build out of the box without any manual submodule patching. --- cmake/Packages/photospline.cmake | 30 ++++ ...-bsplvb_simple-bspline_deriv_nonzero.patch | 163 ++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 cmake/photospline_patches/0001-restore-bsplvb_simple-bspline_deriv_nonzero.patch diff --git a/cmake/Packages/photospline.cmake b/cmake/Packages/photospline.cmake index 89059ffd6..146e78a45 100644 --- a/cmake/Packages/photospline.cmake +++ b/cmake/Packages/photospline.cmake @@ -20,6 +20,36 @@ if(NOT EXISTS "${PROJECT_SOURCE_DIR}/vendor/photospline/CMakeLists.txt") endif() #add_subdirectory(${PROJECT_SOURCE_DIR}/vendor/photospline EXCLUDE_FROM_ALL) + +# Apply local patches to the vendored photospline submodule before including +# it. Upstream photospline @ c6fb3ea (the pin used by SIREN) declares +# bsplvb_simple() and bspline_deriv_nonzero() in the public header but does +# not compile implementations for them -- the inline template code in +# detail/bspline_eval.h calls them, causing undefined-symbol link errors when +# libSIREN is loaded at runtime. Restore the implementations from an earlier +# photospline revision. Each patch is idempotent (--forward skips if already +# applied). +set(_PHOTOSPLINE_PATCH_DIR "${PROJECT_SOURCE_DIR}/cmake/photospline_patches") +if(EXISTS "${_PHOTOSPLINE_PATCH_DIR}") + file(GLOB _PHOTOSPLINE_PATCHES "${_PHOTOSPLINE_PATCH_DIR}/*.patch") + list(SORT _PHOTOSPLINE_PATCHES) + foreach(_patch IN LISTS _PHOTOSPLINE_PATCHES) + message(STATUS "Applying photospline patch: ${_patch}") + execute_process( + COMMAND patch -p1 --forward --silent -i "${_patch}" + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/vendor/photospline" + RESULT_VARIABLE _patch_result + OUTPUT_QUIET ERROR_QUIET + ) + # patch returns 1 if already applied (which is fine); only error on 2+ + if(_patch_result GREATER 1) + message(FATAL_ERROR "Failed to apply photospline patch ${_patch} (exit ${_patch_result})") + endif() + endforeach() +endif() +unset(_PHOTOSPLINE_PATCH_DIR) +unset(_PHOTOSPLINE_PATCHES) + # Override CMAKE_POLICY_VERSION_MINIMUM before adding subdirectory set(TEMP_CMAKE_POLICY_VERSION_MINIMUM ${CMAKE_POLICY_VERSION_MINIMUM}) set(CMAKE_POLICY_VERSION_MINIMUM 3.5) diff --git a/cmake/photospline_patches/0001-restore-bsplvb_simple-bspline_deriv_nonzero.patch b/cmake/photospline_patches/0001-restore-bsplvb_simple-bspline_deriv_nonzero.patch new file mode 100644 index 000000000..08b356aeb --- /dev/null +++ b/cmake/photospline_patches/0001-restore-bsplvb_simple-bspline_deriv_nonzero.patch @@ -0,0 +1,163 @@ +diff --git a/src/core/bspline.cpp b/src/core/bspline.cpp +index 9ac3a48..3e06d7c 100644 +--- a/src/core/bspline.cpp ++++ b/src/core/bspline.cpp +@@ -206,4 +206,158 @@ splineeval(const double *knots, const double *weights, int nknots, double x, int + // return cblas_sdot(totalcoeff, basis1->data, 1, basis2->data, 1); + //} + ++void ++bsplvb_simple(const double *knots, const unsigned nknots, ++ double x, int left, int degree, float* biatx) ++{ ++ int i, j; ++ double saved, term; ++ double delta_l[degree], delta_r[degree]; ++ ++ biatx[0] = 1.0; ++ ++ /* ++ * Handle the (rare) cases where x is outside the full ++ * support of the spline surface. ++ */ ++ if (left == degree-1) ++ while (left >= 0 && x < knots[left]) ++ left--; ++ else if (left == nknots-degree-1) ++ while (left < nknots-1 && x > knots[left+1]) ++ left++; ++ ++ /* ++ * NB: if left < degree-1 or left > nknots-degree-1, ++ * the following loop will dereference addresses ouside ++ * of knots[0:nknots]. While terms involving invalid knot ++ * indices will be discarded, it is important that `knots' ++ * have (maxdegree-1)*sizeof(double) bytes of padding ++ * before and after its valid range to prevent segfaults ++ * (see parsefitstable()). ++ */ ++ for (j = 0; j < degree-1; j++) { ++ delta_r[j] = knots[left+j+1] - x; ++ delta_l[j] = x - knots[left-j]; ++ ++ saved = 0.0; ++ ++ for (i = 0; i < j+1; i++) { ++ term = biatx[i] / (delta_r[i] + delta_l[j-i]); ++ biatx[i] = saved + delta_r[i]*term; ++ saved = delta_l[j-i]*term; ++ } ++ ++ biatx[j+1] = saved; ++ } ++ ++ /* ++ * If left < (spline order), only the first (left+1) ++ * splines are valid; the remainder are utter nonsense. ++ */ ++ if ((i = degree-1-left) > 0) { ++ for (j = 0; j < left+1; j++) ++ biatx[j] = biatx[j+i]; /* Move valid splines over. */ ++ for ( ; j < degree; j++) ++ biatx[j] = 0.0; /* The rest are zero by construction. */ ++ } else if ((i = left+degree+1-nknots) > 0) { ++ for (j = degree-1; j > i-1; j--) ++ biatx[j] = biatx[j-i]; ++ for ( ; j >= 0; j--) ++ biatx[j] = 0.0; ++ } ++} ++ ++void ++bsplvb(const double* knots, const double x, const int left, const int jlow, ++ const int jhigh, float* biatx, double* delta_l, double* delta_r) ++{ ++ int i, j; ++ double saved, term; ++ ++ if (jlow == 0) ++ biatx[0] = 1.0; ++ ++ for (j = jlow; j < jhigh-1; j++) { ++ delta_r[j] = knots[left+j+1] - x; ++ delta_l[j] = x - knots[left-j]; ++ ++ saved = 0.0; ++ ++ for (i = 0; i < j+1; i++) { ++ term = biatx[i] / (delta_r[i] + delta_l[j-i]); ++ biatx[i] = saved + delta_r[i]*term; ++ saved = delta_l[j-i]*term; ++ } ++ ++ biatx[j+1] = saved; ++ } ++} ++ ++ ++void ++bspline_deriv_nonzero(const double* knots, const unsigned nknots, ++ const double x, int left, const int n, float* biatx) ++{ ++ int i, j; ++ double temp, a; ++ double delta_l[n], delta_r[n]; ++ ++ /* Special case for constant splines */ ++ if (n == 0) ++ return; ++ ++ /* ++ * Handle the (rare) cases where x is outside the full ++ * support of the spline surface. ++ */ ++ if (left == n) ++ while (left >= 0 && x < knots[left]) ++ left--; ++ else if (left == nknots-n-2) ++ while (left < nknots-1 && x > knots[left+1]) ++ left++; ++ ++ /* Get the non-zero n-1th order B-splines at x */ ++ bsplvb(knots, x, left, 0 /* jlow */, n /* jhigh */, ++ biatx, delta_l, delta_r); ++ ++ /* ++ * Now, form the derivatives of the nth order B-splines from ++ * linear combinations of the lower-order splines. ++ */ ++ ++ /* ++ * On the last supported segment of the ith nth order spline, ++ * only the i+1th n-1th order spline is nonzero. ++ */ ++ temp = biatx[0]; ++ biatx[0] = - n*temp / ((knots[left+1] - knots[left+1-n])); ++ ++ /* On the middle segments, both the ith and i+1th splines contribute. */ ++ for (i = 1; i < n; i++) { ++ a = n*temp/((knots[left+i] - knots[left+i-n])); ++ temp = biatx[i]; ++ biatx[i] = a - n*temp/(knots[left+i+1] - knots[left+i+1-n]); ++ } ++ /* ++ * On the first supported segment of the i+nth nth order spline, ++ * only the ith n-1th order spline is nonzero. ++ */ ++ biatx[n] = n*temp/((knots[left+n] - knots[left])); ++ ++ /* Rearrange for partially-supported points. */ ++ if ((i = n-left) > 0) { ++ for (j = 0; j < left+1; j++) ++ biatx[j] = biatx[j+i]; /* Move valid splines over. */ ++ for ( ; j < n+1; j++) ++ biatx[j] = 0.0; /* The rest are zero by construction. */ ++ } else if ((i = left+n+2-nknots) > 0) { ++ for (j = n; j > i-1; j--) ++ biatx[j] = biatx[j-i]; ++ for ( ; j >= 0; j--) ++ biatx[j] = 0.0; ++ } ++ ++} + } //namespace photospline From 0abd9f2d6098be441671425ef648b1657de464f3 Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Tue, 14 Apr 2026 13:09:38 -0400 Subject: [PATCH 25/93] SIREN_Controller: back-compat with upstream python-interface refactor Three fixes needed after merging upstream/main (PR #71) so existing scripts that use SIREN_Controller continue to work:\n\n1. python/__init__.py: darknews_version() now probes the specific DarkNews APIs that SIREN_DarkNews.py depends on (phase_space, NuclearTarget, get_decay_momenta_from_vegas_samples). If any are missing, returns None, matching the behavior when DarkNews is not installed. Users with incompatible DarkNews versions (e.g., 0.4.1) no longer hit an ImportError on SIREN_Controller import.\n\n2. python/SIREN_Controller.py (constructor): replace the two removed calls _util.get_material_model_path(experiment) + _util.get_detector_model_path(experiment) -> DetectorModel().LoadMaterialModel/LoadDetectorModel pattern with a single _util.load_detector(experiment) call. Upstream dropped get_material_model_path in favor of the resource loader.\n\n3. python/SIREN_Controller.py (GetFiducialVolume): delegate to _util.get_fiducial_volume(experiment). The local implementation opened _util.get_detector_model_path(self.experiment) as a file, but that function now returns a folder path (IceCube/IceCube-v1/), so the open() raised IsADirectoryError. The upstream util appends /densities.dat internally. --- python/SIREN_Controller.py | 27 ++++----------------------- python/__init__.py | 11 +++++++++-- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/python/SIREN_Controller.py b/python/SIREN_Controller.py index 07dde259f..1ebb248ba 100644 --- a/python/SIREN_Controller.py +++ b/python/SIREN_Controller.py @@ -61,13 +61,9 @@ def __init__(self, events_to_inject, experiment, seed=0): # Empty list for our interaction trees self.events = [] - # Find the density and materials files - materials_file = _util.get_material_model_path(experiment) - detector_model_file = _util.get_detector_model_path(experiment) - - self.detector_model = _detector.DetectorModel() - self.detector_model.LoadMaterialModel(materials_file) - self.detector_model.LoadDetectorModel(detector_model_file) + # Load the detector via upstream load_detector (handles both + # materials.dat and densities.dat in the named detector folder). + self.detector_model = _util.load_detector(experiment) # Define the primary injection and physical process self.primary_injection_process = _injection.PrimaryInjectionProcess() @@ -291,22 +287,7 @@ def GetFiducialVolume(self): """ :return: identified fiducial volume for the experiment, None if not found """ - detector_model_file = _util.get_detector_model_path(self.experiment) - with open(detector_model_file) as file: - fiducial_line = None - detector_line = None - for line in file: - data = line.split() - if len(data) <= 0: - continue - elif data[0] == "fiducial": - fiducial_line = line - elif data[0] == "detector": - detector_line = line - if fiducial_line is None or detector_line is None: - return None - return _detector.DetectorModel.ParseFiducialVolume(fiducial_line, detector_line) - return None + return _util.get_fiducial_volume(self.experiment) def GetCylinderVolumePositionDistributionFromSector(self, sector_name): geo = self.GetDetectorSectorGeometry(sector_name) diff --git a/python/__init__.py b/python/__init__.py index e59cd934b..e161825b8 100644 --- a/python/__init__.py +++ b/python/__init__.py @@ -49,9 +49,16 @@ def darknews_version(): try: import DarkNews + # Require the specific DarkNews APIs that SIREN_DarkNews.py uses. + # Some installed DarkNews versions ship without these, in which case + # we treat DarkNews as unavailable so the SIREN_DarkNews import path + # is skipped (same as when DarkNews itself is not installed). + from DarkNews import phase_space # noqa: F401 + from DarkNews.nuclear_tools import NuclearTarget # noqa: F401 + from DarkNews.integrands import get_decay_momenta_from_vegas_samples # noqa: F401 return _util.normalize_version(DarkNews.__version__) - except: - print("WARNING: DarkNews is not installed in the local environment") + except Exception: + print("WARNING: DarkNews is not available (not installed, or installed version lacks required APIs)") return None utilities.darknews_version = darknews_version From 1c30c18d9faf89adf23a1969f626d5dcb7e315ce Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Tue, 14 Apr 2026 13:15:25 -0400 Subject: [PATCH 26/93] SIREN_Controller: replace old Add*Distribution/Injector API with upstream new-style calls Three more fixes needed after PR #71 for back-compat:\n\n1. Set{Injection,Physical}Processes: upstream removed the AddPrimaryInjectionDistribution / AddSecondaryInjectionDistribution / AddPhysicalDistribution methods from the pybind Process classes in favor of a list-valued property. Accumulate the desired distributions into a local list and assign once.\n\n2. MakeSecondaries (create-on-demand path): same fix; secondary_injection_process.distributions = [SecondaryBoundedVertex.../SecondaryPhysicalVertex...] instead of Add*Distribution.\n\n3. InitializeInjector/InitializeWeighter: _injection.Injector and _injection.Weighter are now python wrappers (Injector.py / Weighter.py) with property-setter APIs, NOT the old positional-args pybind constructors. The controller needs the pybind ones, which are preserved by __init__.py as _injection._Injector / _injection._Weighter. Switch all four constructor call sites to use those underscore-prefixed aliases.\n\nWith these, an end-to-end charm test (NuE CC+NC + CharmHadronization + CharmMesonDecay + DMesonELoss, 50 events, seed=1) generates and saves events cleanly on the merged branch. --- python/SIREN_Controller.py | 76 +++++++++++++++----------------------- 1 file changed, 30 insertions(+), 46 deletions(-) diff --git a/python/SIREN_Controller.py b/python/SIREN_Controller.py index 1ebb248ba..d1d8b1bf0 100644 --- a/python/SIREN_Controller.py +++ b/python/SIREN_Controller.py @@ -97,43 +97,35 @@ def SetInjectionProcesses( :param list secondary_injection_distributions: List of dict of injection distributions for each secondary process """ - # Define the primary injection process primary type + # Define the primary injection process primary type. + # Upstream PR #71 replaced Add*Distribution(...) methods with a + # list-valued `distributions` property. We accumulate the full list + # locally then assign once. self.primary_injection_process.primary_type = primary_type - # Default injection distributions + primary_idist_list = [] if "mass" not in primary_injection_distributions.keys(): - self.primary_injection_process.AddPrimaryInjectionDistribution( - _distributions.PrimaryMass(0) - ) - + primary_idist_list.append(_distributions.PrimaryMass(0)) if "helicity" not in primary_injection_distributions.keys(): - self.primary_injection_process.AddPrimaryInjectionDistribution( - _distributions.PrimaryNeutrinoHelicityDistribution() - ) - - # Add all injection distributions + primary_idist_list.append(_distributions.PrimaryNeutrinoHelicityDistribution()) for _, idist in primary_injection_distributions.items(): - self.primary_injection_process.AddPrimaryInjectionDistribution(idist) + primary_idist_list.append(idist) + self.primary_injection_process.distributions = primary_idist_list # Loop through possible secondary interactions for i_sec, secondary_type in enumerate(secondary_types): secondary_injection_process = _injection.SecondaryInjectionProcess() secondary_injection_process.primary_type = secondary_type - # Add all injection distributions - for idist in secondary_injection_distributions[i_sec]: - secondary_injection_process.AddSecondaryInjectionDistribution(idist) + sec_idist_list = list(secondary_injection_distributions[i_sec]) # Add the position distribution if self.fid_vol is not None: - secondary_injection_process.AddSecondaryInjectionDistribution( - _distributions.SecondaryBoundedVertexDistribution(self.fid_vol) - ) + sec_idist_list.append(_distributions.SecondaryBoundedVertexDistribution(self.fid_vol)) else: - secondary_injection_process.AddSecondaryInjectionDistribution( - _distributions.SecondaryPhysicalVertexDistribution() - ) + sec_idist_list.append(_distributions.SecondaryPhysicalVertexDistribution()) + secondary_injection_process.distributions = sec_idist_list self.secondary_injection_processes.append(secondary_injection_process) def SetPhysicalProcesses( @@ -151,33 +143,25 @@ def SetPhysicalProcesses( :param list secondary_physical_distributions: List of dict of physical distributions for each secondary process """ - # Define the primary physical process primary type + # Define the primary physical process primary type. + # Upstream PR #71 replaced AddPhysicalDistribution(...) with the + # list-valued `distributions` property. self.primary_physical_process.primary_type = primary_type - # Default physical distributions + primary_pdist_list = [] if "mass" not in primary_physical_distributions.keys(): - self.primary_physical_process.AddPhysicalDistribution( - _distributions.PrimaryMass(0) - ) - + primary_pdist_list.append(_distributions.PrimaryMass(0)) if "helicity" not in primary_physical_distributions.keys(): - self.primary_physical_process.AddPhysicalDistribution( - _distributions.PrimaryNeutrinoHelicityDistribution() - ) - - # Add all physical distributions + primary_pdist_list.append(_distributions.PrimaryNeutrinoHelicityDistribution()) for _, pdist in primary_physical_distributions.items(): - self.primary_physical_process.AddPhysicalDistribution(pdist) + primary_pdist_list.append(pdist) + self.primary_physical_process.distributions = primary_pdist_list # Loop through possible secondary interactions for i_sec, secondary_type in enumerate(secondary_types): secondary_physical_process = _injection.PhysicalProcess() secondary_physical_process.primary_type = secondary_type - - # Add all physical distributions - for pdist in secondary_physical_distributions[i_sec]: - secondary_physical_process.AddPhysicalDistribution(pdist) - + secondary_physical_process.distributions = list(secondary_physical_distributions[i_sec]) self.secondary_physical_processes.append(secondary_physical_process) def SetProcesses( @@ -264,13 +248,13 @@ def InputDarkNewsModel(self, primary_type, table_dir, fill_tables_at_start=False # Add the secondary position distribution if self.fid_vol is not None: - secondary_injection_process.AddSecondaryInjectionDistribution( + secondary_injection_process.distributions = [ _distributions.SecondaryBoundedVertexDistribution(self.fid_vol) - ) + ] else: - secondary_injection_process.AddSecondaryInjectionDistribution( + secondary_injection_process.distributions = [ _distributions.SecondaryPhysicalVertexDistribution() - ) + ] self.secondary_injection_processes.append(secondary_injection_process) self.secondary_physical_processes.append(secondary_physical_process) @@ -405,7 +389,7 @@ def InitializeInjector(self, filenames=None): assert(self.primary_injection_process.primary_type is not None) # Use controller injection objects self.injectors.append( - _injection.Injector( + _injection._Injector( self.events_to_inject, self.detector_model, self.primary_injection_process, @@ -418,7 +402,7 @@ def InitializeInjector(self, filenames=None): assert(len(filenames)>0) # require at least one injector filename for filename in filenames: self.injectors.append( - _injection.Injector( + _injection._Injector( self.events_to_inject, filename, self.random, @@ -432,7 +416,7 @@ def InitializeWeighter(self,filename=None): if filename is None: assert(self.primary_physical_process.primary_type is not None) # Use controller physical objects - self.weighter = _injection.Weighter( + self.weighter = _injection._Weighter( self.injectors, self.detector_model, self.primary_physical_process, @@ -440,7 +424,7 @@ def InitializeWeighter(self,filename=None): ) else: # Try initilalizing with the provided filename - self.weighter = _injection.Weighter( + self.weighter = _injection._Weighter( self.injectors, filename ) From 8e58b3287a47db1f71a875437ac41d182fd7e456 Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Tue, 14 Apr 2026 16:22:25 -0400 Subject: [PATCH 27/93] Add DIS_IceCube_charm example using the new Injector/Weighter idiom Sibling of DIS_IceCube.py for charm-DIS event generation. Demonstrates: - QuarkDISFromSpline as primary (NuE CC+NC on O/H, charm-target splines). QuarkDISFromSpline emits D mesons (D0/D+/D0Bar/D+Bar) directly as primary-DIS secondaries; no intermediate Charm quark and no CharmHadronization step are needed. The legacy CharmDISFromSpline + CharmHadronization pathway is superseded. - D+/D0 secondary interactions: DMesonELoss + CharmMesonDecay. - Pure Injector/Weighter property-setter API (no SIREN_Controller). - _util.GenerateEvents/SaveEvents end-to-end. - Inline cylinder-volume-position helper matching the inlined pattern in DIS_ATLAS.py (upstream has no convenience wrapper for this). Verified bit-identical (sum weight, per-type counts, per-type log10E KS tests all match exactly, p=1) against pre-merge dev/DMeson + unchanged Process-SIREN.py over 1000 charm events with seed=1. --- .../examples/example1/DIS_IceCube_charm.py | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 resources/examples/example1/DIS_IceCube_charm.py diff --git a/resources/examples/example1/DIS_IceCube_charm.py b/resources/examples/example1/DIS_IceCube_charm.py new file mode 100644 index 000000000..07cab9098 --- /dev/null +++ b/resources/examples/example1/DIS_IceCube_charm.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +""" +Charm-DIS example using the upstream PR #71 Injector/Weighter idiom. +No SIREN_Controller. + +Demonstrates the full modern charm chain: + primary NuE CC+NC DIS (QuarkDISFromSpline, charm-target splines) + emits {charged lepton, Hadrons (shower), D meson (D0/D+/D0Bar/D+Bar)} + directly — no intermediate "Charm" quark, no separate hadronization step + secondary on each D meson + DMesonELoss (propagation energy loss) + CharmMesonDecay (decay into leptons + K/pi) + +10,000 events on IceCube, seed=1, volume injection inside the icecube sector, +astrophysical power-law weighting. + +Spline paths below are cluster-specific; edit `SPLINES_DIR` to point at your +own set of QuarkDIS charm-target spline files. + +Usage: + python3 charm_example_new_idiom.py +""" + +import os +import numpy as np + +import siren +from siren._util import GenerateEvents, SaveEvents + + +# ---------------------------------------------------------------------------- +# Config (edit for your setup) +# ---------------------------------------------------------------------------- + +SPLINES_DIR = ( + "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/miaochenjin/" + "DBSearch/Simulation/Resources/Splines/M_Muon_New" +) +EXPERIMENT = "IceCube" +PRIMARY_TYPE = siren.dataclasses.ParticleType.NuE +NUMBER_OF_EVENTS = 10_000 +SEED = 1 +GEN_EMIN, GEN_EMAX = 1e2, 1e6 # generation energy range [GeV] +OXYGEN_PDF = "EPPS21nlo_CT18Anlo_O16_central" +HYDROGEN_PDF = "HERAPDF20_NLO_EIG_central" +OUTPUT_PREFIX = "output/charm_example" + + +# ---------------------------------------------------------------------------- +# Helpers +# ---------------------------------------------------------------------------- + +def cylinder_volume_position_distribution(detector_model, sector_name): + """Build a CylinderVolumePositionDistribution for a named detector sector. + + Reproduces `SIREN_Controller.GetCylinderVolumePositionDistributionFromSector` + without the Controller. Mirrors the inline pattern used in upstream + `resources/examples/example1/DIS_ATLAS.py`. + """ + geo = None + for sector in detector_model.Sectors: + if sector.name == sector_name: + geo = sector.geo + break + if geo is None: + raise ValueError(f"Detector sector {sector_name!r} not found") + det_position = detector_model.GeoPositionToDetPosition( + siren.detector.GeometryPosition(geo.placement.Position) + ) + det_rotation = geo.placement.Quaternion + det_placement = siren.geometry.Placement(det_position.get(), det_rotation) + cylinder = siren.geometry.Cylinder( + det_placement, geo.Radius, geo.InnerRadius, geo.Z + ) + return siren.distributions.CylinderVolumePositionDistribution(cylinder) + + +def make_quark_dis_xs(pdf, target, current_type): + """Build one QuarkDISFromSpline for a given PDF / nuclear target / CC or NC.""" + int_type = 1 if current_type == "cc" else 2 + isoscalar_mass = (0.938272 + 0.939565) / 2 + return siren.interactions.QuarkDISFromSpline( + os.path.join(SPLINES_DIR, f"dsdxdy_nu-N-{current_type}-charm-{pdf}.fits"), + os.path.join(SPLINES_DIR, f"sigma_nu-N-{current_type}-charm-{pdf}.fits"), + int(int_type), # interaction type: 1=CC, 2=NC + int(1), # quark type: 1=charm + isoscalar_mass, + 1, # min Q^2 + [PRIMARY_TYPE], + [target], + "m", # mass units + ) + + +# ---------------------------------------------------------------------------- +# Primary interactions +# ---------------------------------------------------------------------------- + +PT = siren.dataclasses.ParticleType + +detector_model = siren.utilities.load_detector(EXPERIMENT) + +primary_interactions = [ + make_quark_dis_xs(OXYGEN_PDF, PT.O16Nucleus, "cc"), + make_quark_dis_xs(HYDROGEN_PDF, PT.HNucleus, "cc"), + make_quark_dis_xs(OXYGEN_PDF, PT.O16Nucleus, "nc"), + make_quark_dis_xs(HYDROGEN_PDF, PT.HNucleus, "nc"), +] + +# Generation energy spectrum: flat power law with index 1 over [emin, emax] +# Astrophysical reweight: E^-2.58 normalized to the HESE flux at 100 TeV +edist_gen = siren.distributions.PowerLaw(1, GEN_EMIN, GEN_EMAX) +edist_phy = siren.distributions.PowerLaw(2.58, 1e2, 1e6) +edist_phy.SetNormalizationAtEnergy(1.68e-18 * 1e4 * 4 * np.pi, 1e5) + +direction = siren.distributions.IsotropicDirection() +position = cylinder_volume_position_distribution(detector_model, "icecube") + +primary_injection_distributions = [ + siren.distributions.PrimaryMass(0), + siren.distributions.PrimaryNeutrinoHelicityDistribution(), + edist_gen, + direction, + position, +] +primary_physical_distributions = [ + siren.distributions.PrimaryMass(0), + siren.distributions.PrimaryNeutrinoHelicityDistribution(), + edist_phy, + direction, +] + + +# ---------------------------------------------------------------------------- +# Secondary interactions (D+ / D0) +# +# QuarkDISFromSpline emits a D meson directly as one of the primary-DIS +# secondaries (not a bare charm quark), so no CharmHadronization step is +# needed. We attach energy-loss and decay to each D species. +# ---------------------------------------------------------------------------- + +secondary_interactions = { + PT.DPlus: [ + siren.interactions.DMesonELoss(), + siren.interactions.CharmMesonDecay(primary_type=PT.DPlus), + ], + PT.D0: [ + siren.interactions.DMesonELoss(), + siren.interactions.CharmMesonDecay(primary_type=PT.D0), + ], +} + +sec_vertex_dist = [siren.distributions.SecondaryPhysicalVertexDistribution()] +secondary_injection_distributions = { + PT.DPlus: sec_vertex_dist, + PT.D0: sec_vertex_dist, +} +secondary_physical_distributions = { + PT.DPlus: [], + PT.D0: [], +} + + +# ---------------------------------------------------------------------------- +# Injector — new idiom, property-setter API +# ---------------------------------------------------------------------------- + +injector = siren.injection.Injector() +injector.seed = SEED +injector.number_of_events = NUMBER_OF_EVENTS +injector.detector_model = detector_model +injector.primary_type = PRIMARY_TYPE +injector.primary_interactions = primary_interactions +injector.primary_injection_distributions = primary_injection_distributions +injector.secondary_interactions = secondary_interactions +injector.secondary_injection_distributions = secondary_injection_distributions +injector.stopping_condition = lambda datum, i: False + +events, gen_times = GenerateEvents(injector) +print(f"Generated {len(events)} events") + + +# ---------------------------------------------------------------------------- +# Weighter — new idiom, built after the injector is materialised +# ---------------------------------------------------------------------------- + +weighter = siren.injection.Weighter() +weighter.injectors = [injector] +weighter.detector_model = detector_model +weighter.primary_type = PRIMARY_TYPE +weighter.primary_interactions = list(primary_interactions) +weighter.primary_physical_distributions = primary_physical_distributions +weighter.secondary_interactions = secondary_interactions +weighter.secondary_physical_distributions = secondary_physical_distributions + + +# ---------------------------------------------------------------------------- +# Save events (.siren_events + .hdf5 + .parquet) +# ---------------------------------------------------------------------------- + +os.makedirs(os.path.dirname(OUTPUT_PREFIX), exist_ok=True) +fid_vol = siren.utilities.get_fiducial_volume(EXPERIMENT) +SaveEvents(events, weighter, gen_times, fid_vol=fid_vol, output_filename=OUTPUT_PREFIX) +print(f"Saved output to {OUTPUT_PREFIX}.*") From 562679d9dc678e68ac4139b3a0fc8ba4730f5d1a Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Sat, 2 May 2026 21:49:06 -0400 Subject: [PATCH 28/93] Remove dead CharmDISFromSpline + CharmHadronization pathway The legacy charm injection path (CharmDISFromSpline emitting a Charm quark, then CharmHadronization converting Charm to D meson) is fully superseded by QuarkDISFromSpline, which emits D mesons directly as primary-DIS secondaries (see QuarkDISFromSpline.cxx:340-361). PythiaDISCrossSection is the future drop-in replacement. Removed: - projects/interactions/{public,private}/...CharmDISFromSpline.{h,cxx} - projects/interactions/{public,private}/...CharmHadronization.{h,cxx} - projects/interactions/private/pybindings/Charm{DIS,}*.h - resources/Examples/DMesonExample/ (legacy example using the dead path; resources/examples/example1/DIS_IceCube_charm.py is the live replacement) Updated: - projects/interactions/CMakeLists.txt: drop the two .cxx from the source list. - projects/interactions/private/pybindings/interactions.cxx: drop Charm{DIS,Had}* includes and register_*() calls. - projects/interactions/private/pybindings/DMesonELoss.h: drop dead CharmDISFromSpline.h include (the binding only consumes DMesonELoss/CrossSection/Particle/InteractionRecord/InteractionSignature from other includes). CharmMesonDecay and DMesonELoss (separate, still-used classes) are untouched. --- projects/interactions/CMakeLists.txt | 2 - .../private/CharmDISFromSpline.cxx | 711 ------------------ .../private/CharmHadronization.cxx | 235 ------ .../private/pybindings/CharmDISFromSpline.h | 88 --- .../private/pybindings/CharmHadronization.h | 38 - .../private/pybindings/DMesonELoss.h | 1 - .../private/pybindings/interactions.cxx | 6 - .../SIREN/interactions/CharmDISFromSpline.h | 166 ---- .../SIREN/interactions/CharmHadronization.h | 93 --- resources/Examples/DMesonExample/DIS_D.py | 151 ---- 10 files changed, 1491 deletions(-) delete mode 100644 projects/interactions/private/CharmDISFromSpline.cxx delete mode 100644 projects/interactions/private/CharmHadronization.cxx delete mode 100644 projects/interactions/private/pybindings/CharmDISFromSpline.h delete mode 100644 projects/interactions/private/pybindings/CharmHadronization.h delete mode 100644 projects/interactions/public/SIREN/interactions/CharmDISFromSpline.h delete mode 100644 projects/interactions/public/SIREN/interactions/CharmHadronization.h delete mode 100644 resources/Examples/DMesonExample/DIS_D.py diff --git a/projects/interactions/CMakeLists.txt b/projects/interactions/CMakeLists.txt index d908ac140..bf9afccb2 100644 --- a/projects/interactions/CMakeLists.txt +++ b/projects/interactions/CMakeLists.txt @@ -16,9 +16,7 @@ LIST (APPEND interactions_SOURCES ${PROJECT_SOURCE_DIR}/projects/interactions/private/DummyCrossSection.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/pyDarkNewsCrossSection.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/pyDarkNewsDecay.cxx - ${PROJECT_SOURCE_DIR}/projects/interactions/private/CharmDISFromSpline.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/Hadronization.cxx - ${PROJECT_SOURCE_DIR}/projects/interactions/private/CharmHadronization.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/CharmMesonDecay.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/CharmMesonDecay3Body.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/DMesonELoss.cxx diff --git a/projects/interactions/private/CharmDISFromSpline.cxx b/projects/interactions/private/CharmDISFromSpline.cxx deleted file mode 100644 index 539eda0f6..000000000 --- a/projects/interactions/private/CharmDISFromSpline.cxx +++ /dev/null @@ -1,711 +0,0 @@ -#include "SIREN/interactions/CharmDISFromSpline.h" - -#include // for map, opera... -#include // for set, opera... -#include // for array -#include // for pow, log10 -#include // for tie, opera... -#include // for allocator -#include // for basic_string -#include // for vector -#include // for assert -#include // for size_t -#include -#include -#include -#include -#include - -#include // for P4, Boost -#include // for Vector3 - -#include // for splinetable -//#include - -#include "SIREN/interactions/CrossSection.h" // for CrossSection -#include "SIREN/dataclasses/InteractionRecord.h" // for Interactio... -#include "SIREN/dataclasses/Particle.h" // for Particle -#include "SIREN/utilities/Random.h" // for SIREN_random -#include "SIREN/utilities/Constants.h" // for electronMass - -namespace siren { -namespace interactions { - -namespace { -///Check whether a given point in phase space is physically realizable. -///Based on equations 6-8 of http://dx.doi.org/10.1103/PhysRevD.66.113007 -///S. Kretzer and M. H. Reno -///"Tau neutrino deep inelastic charged current interactions" -///Phys. Rev. D 66, 113007 -///\param x Bjorken x of the interaction -///\param y Bjorken y of the interaction -///\param E Incoming neutrino in energy in the lab frame ($E_\nu$) -///\param M Mass of the target nucleon ($M_N$) -///\param m Mass of the secondary lepton ($m_\tau$) -bool kinematicallyAllowed(double x, double y, double E, double M, double m) { - if(x > 1) //Eq. 6 right inequality - return false; - // this is to get rid of the infinities as a temporary solution - if (x < 1e-6 or y < 1e-6) return false; - if(x < ((m * m) / (2 * M * (E - m)))) //Eq. 6 left inequality - return false; - //denominator of a and b - double d = 2 * (1 + (M * x) / (2 * E)); - //the numerator of a (or a*d) - double ad = 1 - m * m * ((1 / (2 * M * E * x)) + (1 / (2 * E * E))); - double term = 1 - ((m * m) / (2 * M * E * x)); - //the numerator of b (or b*d) - double bd = sqrt(term * term - ((m * m) / (E * E))); - - double s = 2 * M * E; - double Q2 = s * x * y; - double Mc = siren::utilities::Constants::D0Mass; - return ((ad - bd) <= d * y and d * y <= (ad + bd)) && (Q2 / (1 - x) + pow(M, 2) >= pow(M + Mc, 2)); //Eq. 7 -} -} - -CharmDISFromSpline::CharmDISFromSpline() {} - -CharmDISFromSpline::CharmDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { - LoadFromMemory(differential_data, total_data); - InitializeSignatures(); - SetUnits(units); -} - -CharmDISFromSpline::CharmDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { - LoadFromMemory(differential_data, total_data); - InitializeSignatures(); - SetUnits(units); -} - -CharmDISFromSpline::CharmDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { - LoadFromFile(differential_filename, total_filename); - InitializeSignatures(); - SetUnits(units); -} - -CharmDISFromSpline::CharmDISFromSpline(std::string differential_filename, std::string total_filename, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types) { - LoadFromFile(differential_filename, total_filename); - ReadParamsFromSplineTable(); - InitializeSignatures(); - SetUnits(units); -} - -CharmDISFromSpline::CharmDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { - LoadFromFile(differential_filename, total_filename); - InitializeSignatures(); - SetUnits(units); -} - -CharmDISFromSpline::CharmDISFromSpline(std::string differential_filename, std::string total_filename, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()) { - LoadFromFile(differential_filename, total_filename); - ReadParamsFromSplineTable(); - InitializeSignatures(); - SetUnits(units); -} - -void CharmDISFromSpline::SetUnits(std::string units) { - std::transform(units.begin(), units.end(), units.begin(), - [](unsigned char c){ return std::tolower(c); }); - if(units == "cm") { - unit = 1.0; - } else if(units == "m") { - unit = 10000.0; - } else { - throw std::runtime_error("Cross section units not supported!"); - } -} - -void CharmDISFromSpline::SetInteractionType(int interaction) { - interaction_type_ = interaction; -} - -bool CharmDISFromSpline::equal(CrossSection const & other) const { - const CharmDISFromSpline* x = dynamic_cast(&other); - - if(!x) - return false; - else - return - std::tie( - interaction_type_, - target_mass_, - minimum_Q2_, - signatures_, - primary_types_, - target_types_, - differential_cross_section_, - total_cross_section_) - == - std::tie( - x->interaction_type_, - x->target_mass_, - x->minimum_Q2_, - x->signatures_, - x->primary_types_, - x->target_types_, - x->differential_cross_section_, - x->total_cross_section_); -} - -void CharmDISFromSpline::LoadFromFile(std::string dd_crossSectionFile, std::string total_crossSectionFile) { - - differential_cross_section_ = photospline::splinetable<>(dd_crossSectionFile.c_str()); - - if(differential_cross_section_.get_ndim()!=3 && differential_cross_section_.get_ndim()!=2) - throw std::runtime_error("cross section spline has " + std::to_string(differential_cross_section_.get_ndim()) - + " dimensions, should have either 3 (log10(E), log10(x), log10(y)) or 2 (log10(E), log10(y))"); - - total_cross_section_ = photospline::splinetable<>(total_crossSectionFile.c_str()); - - if(total_cross_section_.get_ndim() != 1) - throw std::runtime_error("Total cross section spline has " + std::to_string(total_cross_section_.get_ndim()) - + " dimensions, should have 1, log10(E)"); -} - -void CharmDISFromSpline::LoadFromMemory(std::vector & differential_data, std::vector & total_data) { - differential_cross_section_.read_fits_mem(differential_data.data(), differential_data.size()); - total_cross_section_.read_fits_mem(total_data.data(), total_data.size()); -} - -double CharmDISFromSpline::GetLeptonMass(siren::dataclasses::ParticleType lepton_type) { - int32_t lepton_number = std::abs(static_cast(lepton_type)); - double lepton_mass; - switch(lepton_number) { - case 11: - lepton_mass = siren::utilities::Constants::electronMass; - break; - case 13: - lepton_mass = siren::utilities::Constants::muonMass; - break; - case 15: - lepton_mass = siren::utilities::Constants::tauMass; - break; - case 12: - lepton_mass = 0; - case 14: - lepton_mass = 0; - case 16: - lepton_mass = 0; - break; - default: - throw std::runtime_error("Unknown lepton type!"); - } - return lepton_mass; -} - -void CharmDISFromSpline::ReadParamsFromSplineTable() { - // returns true if successfully read target mass - bool mass_good = differential_cross_section_.read_key("TARGETMASS", target_mass_); - if (mass_good) {std::cout << "read target mass!!" << std::endl;} // for debugging purposes - // returns true if successfully read interaction type - bool int_good = differential_cross_section_.read_key("INTERACTION", interaction_type_); - // returns true if successfully read minimum Q2 - bool q2_good = differential_cross_section_.read_key("Q2MIN", minimum_Q2_); - - if(!int_good) { - // assume DIS to preserve compatability with previous versions - interaction_type_ = 1; - } - - if(!q2_good) { - // assume 1 GeV^2 - minimum_Q2_ = 1; - } - - std::cout << "Q2 good status is " << q2_good << "and is set to " << minimum_Q2_; - - if(!mass_good) { - if(int_good) { - if(interaction_type_ == 1 or interaction_type_ == 2) { - target_mass_ = (siren::dataclasses::isLepton(siren::dataclasses::ParticleType::PPlus)+ - siren::dataclasses::isLepton(siren::dataclasses::ParticleType::Neutron))/2; - } else if(interaction_type_ == 3) { - target_mass_ = siren::dataclasses::isLepton(siren::dataclasses::ParticleType::EMinus); - } else { - throw std::runtime_error("Logic error. Interaction type is not 1, 2, or 3!"); - } - - } else { - if(differential_cross_section_.get_ndim() == 3) { - target_mass_ = siren::utilities::Constants::isoscalarMass; - - } else if(differential_cross_section_.get_ndim() == 2) { - target_mass_ = siren::utilities::Constants::electronMass; - } else { - throw std::runtime_error("Logic error. Spline dimensionality is not 2, or 3!"); - } - } - } - std::cout << "target mass is " << target_mass_ << std::endl; - -} - -void CharmDISFromSpline::InitializeSignatures() { - signatures_.clear(); - for(auto primary_type : primary_types_) { - dataclasses::InteractionSignature signature; - signature.primary_type = primary_type; - - if(not isNeutrino(primary_type)) { - throw std::runtime_error("This DIS implementation only supports neutrinos as primaries!"); - } - - siren::dataclasses::ParticleType charged_lepton_product = siren::dataclasses::ParticleType::unknown; - siren::dataclasses::ParticleType neutral_lepton_product = primary_type; - - if(primary_type == siren::dataclasses::ParticleType::NuE) { - charged_lepton_product = siren::dataclasses::ParticleType::EMinus; - } else if(primary_type == siren::dataclasses::ParticleType::NuEBar) { - charged_lepton_product = siren::dataclasses::ParticleType::EPlus; - } else if(primary_type == siren::dataclasses::ParticleType::NuMu) { - charged_lepton_product = siren::dataclasses::ParticleType::MuMinus; - } else if(primary_type == siren::dataclasses::ParticleType::NuMuBar) { - charged_lepton_product = siren::dataclasses::ParticleType::MuPlus; - } else if(primary_type == siren::dataclasses::ParticleType::NuTau) { - charged_lepton_product = siren::dataclasses::ParticleType::TauMinus; - } else if(primary_type == siren::dataclasses::ParticleType::NuTauBar) { - charged_lepton_product = siren::dataclasses::ParticleType::TauPlus; - } else { - throw std::runtime_error("InitializeSignatures: Unkown parent neutrino type!"); - } - - if(interaction_type_ == 1) { - signature.secondary_types.push_back(charged_lepton_product); - } else if(interaction_type_ == 2) { - signature.secondary_types.push_back(neutral_lepton_product); - } else if(interaction_type_ == 3) { - signature.secondary_types.push_back(siren::dataclasses::ParticleType::Hadrons); - } else { - throw std::runtime_error("InitializeSignatures: Unkown interaction type!"); - } - - signature.secondary_types.push_back(siren::dataclasses::ParticleType::Charm); - for(auto target_type : target_types_) { - signature.target_type = target_type; - - signatures_.push_back(signature); - - std::pair key(primary_type, target_type); - signatures_by_parent_types_[key].push_back(signature); - } - } -} - -double CharmDISFromSpline::TotalCrossSection(dataclasses::InteractionRecord const & interaction) const { - siren::dataclasses::ParticleType primary_type = interaction.signature.primary_type; - rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); - double primary_energy; - primary_energy = interaction.primary_momentum[0]; - // if we are below threshold, return 0 - if(primary_energy < InteractionThreshold(interaction)) { - std::cout << "DIS::interaction threshold not satisfied" << std::endl; - return 0; - } - return TotalCrossSection(primary_type, primary_energy); -} - -double CharmDISFromSpline::TotalCrossSection(siren::dataclasses::ParticleType primary_type, double primary_energy) const { - if(not primary_types_.count(primary_type)) { - throw std::runtime_error("Supplied primary not supported by cross section!"); - } - double log_energy = log10(primary_energy); - - if(log_energy < total_cross_section_.lower_extent(0) - or log_energy > total_cross_section_.upper_extent(0)) { - throw std::runtime_error("Interaction energy ("+ std::to_string(primary_energy) + - ") out of cross section table range: [" - + std::to_string(pow(10.,total_cross_section_.lower_extent(0))) + " GeV," - + std::to_string(pow(10.,total_cross_section_.upper_extent(0))) + " GeV]"); - } - - int center; - total_cross_section_.searchcenters(&log_energy, ¢er); - - double log_xs = total_cross_section_.ndsplineeval(&log_energy, ¢er, 0); - if (std::pow(10.0, log_xs) == 0) { - std::cout << "DIS::cross section evaluated to 0" << std::endl; - } - - return unit * std::pow(10.0, log_xs); -} - -double CharmDISFromSpline::DifferentialCrossSection(dataclasses::InteractionRecord const & interaction) const { - rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); - rk::P4 p2(geom3::Vector3(0, 0, 0), interaction.target_mass); - double primary_energy; - primary_energy = interaction.primary_momentum[0]; - assert(interaction.signature.secondary_types.size() == 2); - unsigned int lepton_index = (isLepton(interaction.signature.secondary_types[0])) ? 0 : 1; - unsigned int other_index = 1 - lepton_index; - - std::array const & mom3 = interaction.secondary_momenta[lepton_index]; - std::array const & mom4 = interaction.secondary_momenta[other_index]; - rk::P4 p3(geom3::Vector3(mom3[1], mom3[2], mom3[3]), interaction.secondary_masses[lepton_index]); - rk::P4 p4(geom3::Vector3(mom4[1], mom4[2], mom4[3]), interaction.secondary_masses[other_index]); - - rk::P4 q = p1 - p3; - - double Q2 = -q.dot(q); - double x, y; - double lepton_mass = GetLeptonMass(interaction.signature.secondary_types[lepton_index]); - - - y = 1.0 - p2.dot(p3) / p2.dot(p1); - x = Q2 / (2.0 * p2.dot(q)); - double log_energy = log10(primary_energy); - std::array coordinates{{log_energy, log10(x), log10(y)}}; - std::array centers; - - - if (Q2 < minimum_Q2_ || !kinematicallyAllowed(x, y, primary_energy, target_mass_, lepton_mass) - || !differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { - // std::cout << "weighting: revert back to saved x and y" << std::endl; - double E1_lab = interaction.interaction_parameters.at("energy"); - double E2_lab = p2.e(); - x = interaction.interaction_parameters.at("bjorken_x"); - y = interaction.interaction_parameters.at("bjorken_y"); - Q2 = 2. * E1_lab * E2_lab * x * y; - } - return DifferentialCrossSection(primary_energy, x, y, lepton_mass, Q2); - - -} - -double CharmDISFromSpline::DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2) const { - double log_energy = log10(energy); - // check preconditions - if(log_energy < differential_cross_section_.lower_extent(0) - || log_energy>differential_cross_section_.upper_extent(0)) - {std::cout << "Diff xsec: not in bounds" << std::endl; - return 0.0;} - if(x <= 0 || x >= 1) { - std::cout << "x is out of bounds with x = " << x << std::endl; - return 0.0; - } - if(y <= 0 || y >= 1){ - std::cout << "y is out of bounds with x = " << y << std::endl; - return 0.0; - } - - // we assume that: - // the target is stationary so its energy is just its mass - // the incoming neutrino is massless, so its kinetic energy is its total energy - if(std::isnan(Q2)) { - Q2 = 2.0 * energy * target_mass_ * x * y; - } - if(Q2 < minimum_Q2_) { - std::cout << "Q2 is smaller than minimum Q2 with " << Q2 << " < " << minimum_Q2_ << std::endl; - return 0; - } // cross section not calculated, assumed to be zero - - if(!kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) { - std::cout << "not kinematically allowed!" << std::endl; - return 0; - } - std::array coordinates{{log_energy, log10(x), log10(y)}}; - std::array centers; - if(!differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { - std::cout << "search centers failed!" << std::endl; - return 0; - } - double result = pow(10., differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0)); - assert(result >= 0); - if (std::isinf(result)) { - std::cout << "energy, x, y, Q2 are " << energy << " " << x << " " << y << " " << Q2 << " " << std::endl; - std::cout << "spline value read is " << differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0) << std::endl; - } - - return unit * result; -} - -double CharmDISFromSpline::InteractionThreshold(dataclasses::InteractionRecord const & interaction) const { - // Consider implementing DIS thershold at some point - return 0; -} - -void CharmDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { - // Uses Metropolis-Hastings Algorithm! - // useful for cases where we don't know the supremum of our distribution, and the distribution is multi-dimensional - if (differential_cross_section_.get_ndim() != 3) { - throw std::runtime_error("I expected 3 dimensions in the cross section spline, but got " + std::to_string(differential_cross_section_.get_ndim()) +". Maybe your fits file doesn't have the right 'INTERACTION' key?"); - } - rk::P4 p1(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); - rk::P4 p2(geom3::Vector3(0, 0, 0), record.target_mass); - - // we assume that: - // the target is stationary so its energy is just its mass - // the incoming neutrino is massless, so its kinetic energy is its total energy - // double s = target_mass_ * trecord.secondary_momentarget_mass_ + 2 * target_mass_ * primary_energy; - // double s = std::pow(rk::invMass(p1, p2), 2); - - double primary_energy; - rk::P4 p1_lab; - rk::P4 p2_lab; - p1_lab = p1; - p2_lab = p2; - primary_energy = p1_lab.e(); - - unsigned int lepton_index = (isLepton(record.signature.secondary_types[0])) ? 0 : 1; - unsigned int other_index = 1 - lepton_index; - double m = GetLeptonMass(record.signature.secondary_types[lepton_index]); - - double m1 = record.primary_mass; - double m3 = m; - double E1_lab = p1_lab.e(); - double E2_lab = p2_lab.e(); - - // The out-going particle always gets at least enough energy for its rest mass - double yMax = 1 - m / primary_energy; - double logYMax = log10(yMax); - - // The minimum allowed value of y occurs when x = 1 and Q is minimized - double yMin = minimum_Q2_ / (2 * E1_lab * E2_lab); - double logYMin = log10(yMin); - // The minimum allowed value of x occurs when y = yMax and Q is minimized - // double xMin = minimum_Q2_ / ((s - target_mass_ * target_mass_) * yMax); - double xMin = minimum_Q2_ / (2 * E1_lab * E2_lab * yMax); - double logXMin = log10(xMin); - - bool accept; - - // kin_vars and its twin are 3-vectors containing [nu-energy, Bjorken X, Bjorken Y] - std::array kin_vars, test_kin_vars; - - // centers of the cross section spline tables. - std::array spline_table_center, test_spline_table_center; - - // values of cross_section from the splines. By * Bx * Spline(E,x,y) - double cross_section, test_cross_section; - - // No matter what, we're evaluating at this specific energy. - kin_vars[0] = test_kin_vars[0] = log10(primary_energy); - - // check preconditions - if(kin_vars[0] < differential_cross_section_.lower_extent(0) - || kin_vars[0] > differential_cross_section_.upper_extent(0)) - throw std::runtime_error("Interaction energy out of cross section table range: [" - + std::to_string(pow(10.,differential_cross_section_.lower_extent(0))) + " GeV," - + std::to_string(pow(10.,differential_cross_section_.upper_extent(0))) + " GeV]"); - - // sample an intial point - do { - // rejection sample a point which is kinematically allowed by calculation limits - double trialQ; - double trials = 0; - do { - if (trials >= 100) throw std::runtime_error("too much trials"); - trials += 1; - kin_vars[1] = random->Uniform(logXMin,0); - kin_vars[2] = random->Uniform(logYMin,logYMax); - trialQ = (2 * E1_lab * E2_lab) * pow(10., kin_vars[1] + kin_vars[2]); - } while(trialQ differential_cross_section_.upper_extent(1)) { - accept = false; - } - if(kin_vars[2] < differential_cross_section_.lower_extent(2) - || kin_vars[2] > differential_cross_section_.upper_extent(2)) { - accept = false; - } - - if(accept) { - // finds the centers in the cross section spline table, returns true if it's successful - // also sets the centers - accept = differential_cross_section_.searchcenters(kin_vars.data(),spline_table_center.data()); - } - } while(!accept); - - //TODO: better proposal distribution? - double measure = pow(10., kin_vars[1] + kin_vars[2]); // Bx * By - - // Bx * By * xs(E, x, y) - // evalutates the differential spline at that point - cross_section = measure*pow(10., differential_cross_section_.ndsplineeval(kin_vars.data(), spline_table_center.data(), 0)); - - // this is the magic part. Metropolis Hastings Algorithm. - // MCMC method! - const size_t burnin = 40; // converges to the correct distribution over multiple samplings. - // big number means more accurate, but slower - for(size_t j = 0; j <= burnin; j++) { - // repeat the sampling from above to get a new valid point - double trialQ; - do { - test_kin_vars[1] = random->Uniform(logXMin, 0); - test_kin_vars[2] = random->Uniform(logYMin, logYMax); - trialQ = (2 * E1_lab * E2_lab) * pow(10., test_kin_vars[1] + test_kin_vars[2]); - } while(trialQ < minimum_Q2_ || !kinematicallyAllowed(pow(10., test_kin_vars[1]), pow(10., test_kin_vars[2]), primary_energy, target_mass_, m)); - - accept = true; - if(test_kin_vars[1] < differential_cross_section_.lower_extent(1) - || test_kin_vars[1] > differential_cross_section_.upper_extent(1)) - accept = false; - if(test_kin_vars[2] < differential_cross_section_.lower_extent(2) - || test_kin_vars[2] > differential_cross_section_.upper_extent(2)) - accept = false; - if(!accept) - continue; - - accept = differential_cross_section_.searchcenters(test_kin_vars.data(), test_spline_table_center.data()); - if(!accept) - continue; - - double measure = pow(10., test_kin_vars[1] + test_kin_vars[2]); - double eval = differential_cross_section_.ndsplineeval(test_kin_vars.data(), test_spline_table_center.data(), 0); - if(std::isnan(eval)) - continue; - test_cross_section = measure * pow(10., eval); - - double odds = (test_cross_section / cross_section); - accept = (cross_section == 0 || (odds > 1.) || random->Uniform(0, 1) < odds); - - if(accept) { - kin_vars = test_kin_vars; - cross_section = test_cross_section; - // std::cout << "trial Q is" << trialQ << std::endl; - } - } - //////////////////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////////////////// - double final_x = pow(10., kin_vars[1]); - double final_y = pow(10., kin_vars[2]); - record.interaction_parameters.clear(); - record.interaction_parameters["energy"] = E1_lab; - record.interaction_parameters["bjorken_x"] = final_x; - record.interaction_parameters["bjorken_y"] = final_y; - - double Q2 = 2 * E1_lab * E2_lab * pow(10.0, kin_vars[1] + kin_vars[2]); - double p1x_lab = std::sqrt(p1_lab.px() * p1_lab.px() + p1_lab.py() * p1_lab.py() + p1_lab.pz() * p1_lab.pz()); - double pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); - double momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); - double pqy_lab, Eq_lab; - - if (pqx_lab>momq_lab){ - // if current setting does not work, start looping through scalings - int maxIterations = 10; - int iteration = 0; - double p1_lab_x = p1_lab.px(); - double p1_lab_y = p1_lab.py(); - double p1_lab_z = p1_lab.pz(); - // loop to resolve precision issue - while (iteration <= maxIterations) { - Q2 = 2. * E1_lab * E2_lab * pow(10.0, kin_vars[1] + kin_vars[2]); - p1x_lab = std::sqrt(p1_lab_x * p1_lab_x + p1_lab_y * p1_lab_y + p1_lab_z * p1_lab_z); - pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); - momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); - if (pqx_lab>momq_lab){ - // std::cout << "triggered on " << momq_lab << " and " << pqx_lab << std::endl; - //scale down - E1_lab /= 10; - E2_lab /= 10; - p1_lab_x /= 10; - p1_lab_y /= 10; - p1_lab_z /= 10; - m1 /= 10; - m3 /= 10; - //iteration += 1 to scale back - iteration += 1; - continue; - } - pqy_lab = std::sqrt((momq_lab + pqx_lab) * (momq_lab - pqx_lab)); - // std::cout << "finished with " << iteration << " iterations and " << momq_lab << " and " << pqx_lab << std::endl; - break; - } - // //scale back - if (iteration > 0) { - // std::cout << "scaling back with " << pow(10.0, iteration); - E1_lab *= pow(10.0, iteration); - E2_lab *= pow(10.0, iteration); - p1_lab_x *= pow(10.0, iteration); - p1_lab_y *= pow(10.0, iteration); - p1_lab_z *= pow(10.0, iteration); - m1 *= pow(10.0, iteration); - m3 *= pow(10.0, iteration); - // std::cout << "and finished with " << momq_lab << " and " << pqx_lab << std::endl; - } - // pqy_lab = 0; - } else {pqy_lab = std::sqrt(momq_lab*momq_lab - pqx_lab *pqx_lab);} - Eq_lab = E1_lab * final_y; - - geom3::UnitVector3 x_dir = geom3::UnitVector3::xAxis(); - geom3::Vector3 p1_mom = p1_lab.momentum(); - geom3::UnitVector3 p1_lab_dir = p1_mom.direction(); - geom3::Rotation3 x_to_p1_lab_rot = geom3::rotationBetween(x_dir, p1_lab_dir); - - double phi = random->Uniform(0, 2.0 * M_PI); - geom3::Rotation3 rand_rot(p1_lab_dir, phi); - - rk::P4 pq_lab(Eq_lab, geom3::Vector3(pqx_lab, pqy_lab, 0)); - pq_lab.rotate(x_to_p1_lab_rot); - pq_lab.rotate(rand_rot); - - rk::P4 p3_lab((p1_lab - pq_lab).momentum(), m3); - rk::P4 p4_lab = p2_lab + pq_lab; - - rk::P4 p3; - rk::P4 p4; - p3 = p3_lab; - p4 = p4_lab; - - std::vector & secondaries = record.GetSecondaryParticleRecords(); - siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[lepton_index]; - siren::dataclasses::SecondaryParticleRecord & other = secondaries[other_index]; - - lepton.SetFourMomentum({p3.e(), p3.px(), p3.py(), p3.pz()}); - lepton.SetMass(p3.m()); - lepton.SetHelicity(record.primary_helicity); - other.SetFourMomentum({p4.e(), p4.px(), p4.py(), p4.pz()}); - other.SetMass(p4.m()); - other.SetHelicity(record.target_helicity); -} - -double CharmDISFromSpline::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { - double dxs = DifferentialCrossSection(interaction); - // if (dxs == 0) { - // std::cout << "diff xsec gives 0" << std::endl; - // } - double txs = TotalCrossSection(interaction); - if(dxs == 0) { - return 0.0; - } else { - // if (txs == 0) {std::cout << "wtf??? txs is 0 in final state prob" << txs << std::endl;} - // if (std::isinf(dxs)) {std::cout << "dxs is inf in final state prob" << std::endl;} - return dxs / txs; - } -} - -std::vector CharmDISFromSpline::GetPossiblePrimaries() const { - return std::vector(primary_types_.begin(), primary_types_.end()); -} - -std::vector CharmDISFromSpline::GetPossibleTargetsFromPrimary(siren::dataclasses::ParticleType primary_type) const { - return std::vector(target_types_.begin(), target_types_.end()); -} - -std::vector CharmDISFromSpline::GetPossibleSignatures() const { - return std::vector(signatures_.begin(), signatures_.end()); -} - -std::vector CharmDISFromSpline::GetPossibleTargets() const { - return std::vector(target_types_.begin(), target_types_.end()); -} - -std::vector CharmDISFromSpline::GetPossibleSignaturesFromParents(siren::dataclasses::ParticleType primary_type, siren::dataclasses::ParticleType target_type) const { - std::pair key(primary_type, target_type); - if(signatures_by_parent_types_.find(key) != signatures_by_parent_types_.end()) { - return signatures_by_parent_types_.at(key); - } else { - return std::vector(); - } -} - -std::vector CharmDISFromSpline::DensityVariables() const { - return std::vector{"Bjorken x", "Bjorken y"}; -} - -} // namespace interactions -} // namespace siren diff --git a/projects/interactions/private/CharmHadronization.cxx b/projects/interactions/private/CharmHadronization.cxx deleted file mode 100644 index 008b608ff..000000000 --- a/projects/interactions/private/CharmHadronization.cxx +++ /dev/null @@ -1,235 +0,0 @@ -#include "SIREN/interactions/CharmHadronization.h" - -#include // for array -#include // for sqrt, M_PI -#include // for basic_s... -#include // for vector -#include // for size_t - -#include // for Vector3 -#include // for P4, Boost - -#include "SIREN/interactions/Hadronization.h" // for Hadronization -#include "SIREN/dataclasses/InteractionRecord.h" // for Interac... -#include "SIREN/dataclasses/InteractionSignature.h" // for Interac... -#include "SIREN/dataclasses/Particle.h" // for Particle -#include "SIREN/utilities/Random.h" // for SIREN_random -#include "SIREN/utilities/Errors.h" // for PythonImplementationError -#include "SIREN/utilities/Constants.h" // for electronMass - - - -namespace siren { -namespace interactions { - -CharmHadronization::CharmHadronization() { - // initialize the pdf normalization and cdf table - normalize_pdf(); - compute_cdf(); -} - -// pybind11::object CharmHadronization::get_self() { -// return pybind11::cast(Py_None); -// } - -bool CharmHadronization::equal(Hadronization const & other) const { - const CharmHadronization* x = dynamic_cast(&other); - - if(!x) - return false; - else - return primary_types == x->primary_types; -} - -void CharmHadronization::normalize_pdf() { - if (fragmentation_integral == 0){ - std::function integrand = [&] (double x) -> double { - return (0.8 / x ) / (std::pow(1 - (1 / x) - (0.2 / (1 - x)), 2)); - }; - fragmentation_integral = siren::utilities::rombergIntegrate(integrand, 0.001, 0.999); - } else { - std::cout << "Something is wrong... you already computed the normalization" << std::endl; - return; - } -} - -double CharmHadronization::sample_pdf(double x) const { - return (0.8 / x ) / (std::pow(1 - (1 / x) - (0.2 / (1 - x)), 2)) / fragmentation_integral; -} - -void CharmHadronization::compute_cdf() { - // first set the z nodes - std::vector zspline; - for (int i = 0; i < 100; ++i) { - zspline.push_back(0.01 + i * (0.99-0.01) / 100 ); - } - - // declare the cdf vectors - std::vector cdf_vector; - std::vector cdf_z_nodes; - std::vector pdf_vector; - - cdf_z_nodes.push_back(0); - cdf_vector.push_back(0); - pdf_vector.push_back(0); - - // compute the spline table - for (int i = 0; i < zspline.size(); ++i) { - if (i == 0) { - double cur_z = zspline[i]; - double cur_pdf = sample_pdf(cur_z); - double area = cur_z * cur_pdf * 0.5; - pdf_vector.push_back(cur_pdf); - cdf_vector.push_back(area); - cdf_z_nodes.push_back(cur_z); - continue; - } - double cur_z = zspline[i]; - double cur_pdf = sample_pdf(cur_z); - double area = 0.5 * (pdf_vector[i - 1] + cur_pdf) * (zspline[i] - zspline[i - 1]); - pdf_vector.push_back(cur_pdf); - cdf_z_nodes.push_back(cur_z); - cdf_vector.push_back(area + cdf_vector.back()); - } - - cdf_z_nodes.push_back(1); - cdf_vector.push_back(1); - pdf_vector.push_back(0); - - - // set the spline table - siren::utilities::TableData1D inverse_cdf_data; - inverse_cdf_data.x = cdf_vector; - inverse_cdf_data.f = cdf_z_nodes; - - inverseCdfTable = siren::utilities::Interpolator1D(inverse_cdf_data); - - return; -} - -double CharmHadronization::getHadronMass(siren::dataclasses::ParticleType hadron_type) { - switch(hadron_type){ - case siren::dataclasses::ParticleType::D0: - return( siren::utilities::Constants::D0Mass); - case siren::dataclasses::ParticleType::D0Bar: - return( siren::utilities::Constants::D0Mass); - case siren::dataclasses::ParticleType::DPlus: - return( siren::utilities::Constants::DPlusMass); - case siren::dataclasses::ParticleType::DMinus: - return( siren::utilities::Constants::DPlusMass); - case siren::dataclasses::ParticleType::Charm: - return( siren::utilities::Constants::CharmMass); - case siren::dataclasses::ParticleType::CharmBar: - return( siren::utilities::Constants::CharmMass); - default: - return(0.0); - } -} - - -std::vector CharmHadronization::GetPossibleSignatures() const -{ - std::vector signatures; - for(auto primary : primary_types) { - std::vector new_signatures = GetPossibleSignaturesFromParent(primary); - signatures.insert(signatures.end(),new_signatures.begin(),new_signatures.end()); - } - return signatures; -} - -std::vector CharmHadronization::GetPossibleSignaturesFromParent(siren::dataclasses::Particle::ParticleType primary) const { - std::vector signatures; - dataclasses::InteractionSignature signature; - signature.primary_type = primary; - signature.target_type = siren::dataclasses::Particle::ParticleType::Hadronization; - - signature.secondary_types.resize(2); - signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; - if(primary==siren::dataclasses::Particle::ParticleType::Charm) { - signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::D0; - signatures.push_back(signature); - signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::DPlus; - signatures.push_back(signature); - } - else if(primary==siren::dataclasses::Particle::ParticleType::CharmBar) { - signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::D0Bar; - signatures.push_back(signature); - signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::DMinus; - signatures.push_back(signature); - } - return signatures; -} - -void CharmHadronization::SampleFinalState(dataclasses::CrossSectionDistributionRecord & interaction, std::shared_ptr random) const { - // Uses Perterson distribution with epsilon = 0.2 - // charm quark momentum - rk::P4 pc(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); - double p3c = std::sqrt(std::pow(interaction.primary_momentum[1], 2) + std::pow(interaction.primary_momentum[2], 2) + std::pow(interaction.primary_momentum[3], 2)); - double Ec = pc.e(); //energy of primary charm - double mCH = getHadronMass(interaction.signature.secondary_types[1]); // obtain charmed hadron mass - - bool accept; - double randValue; - double z; - double ECH; - - // add a maximum number of trials in the while loop - int max_sampling = 100; - int sampling = 0; - - // sample again if this eenrgy is not kinematically allowed - do { - sampling += 1; - if (sampling > max_sampling) { - std::cout << "energy of the charm is " << Ec << " and momentum is " << p3c << std::endl; - std::cout << "desired mass of hadron is " << mCH << std::endl; - throw(siren::utilities::InjectionFailure("Failed to sample hadronization!")); - break; - } - randValue = random->Uniform(0,1); - z = inverseCdfTable(randValue); - ECH = z * Ec; - if (std::pow(ECH, 2) - std::pow(mCH, 2) <= 0) { - accept = false; - } else { - accept = true; - } - double new_debug = std::pow(ECH, 2) - std::pow(mCH, 2); - } while (!accept); - - // is it ok to compute everything in lab frame? - double p3CH = std::sqrt(std::pow(ECH, 2) - std::pow(mCH, 2)); //obtain charmed hadron 3-momentum - double rCH = p3CH/p3c; // ratio of momentum carried away by the charmed hadron, assume collinearity - rk::P4 p4CH(geom3::Vector3(rCH * interaction.primary_momentum[1], rCH * interaction.primary_momentum[2], rCH * interaction.primary_momentum[3]), mCH); - - double EX = (1 - z) * Ec; // energy of the hadronic shower - double p3X = EX; // assume no hadronic mass - double rX = p3X/p3c; // assume collinear - rk::P4 p4X(geom3::Vector3(rX * interaction.primary_momentum[1], rX * interaction.primary_momentum[2], rX * interaction.primary_momentum[3]), 0); - - // new implementation of updateing outgoing particles - std::vector & secondaries = interaction.GetSecondaryParticleRecords(); - siren::dataclasses::SecondaryParticleRecord & hadronic_vertex = secondaries[0]; - siren::dataclasses::SecondaryParticleRecord & d_meson = secondaries[1]; // these indices are hard-coded, should be automated in a future time - - hadronic_vertex.SetFourMomentum({p4X.e(), p4X.px(), p4X.py(), p4X.pz()}); - hadronic_vertex.SetMass(p4X.m()); - hadronic_vertex.SetHelicity(interaction.primary_helicity); - - d_meson.SetFourMomentum({p4CH.e(), p4CH.px(), p4CH.py(), p4CH.pz()}); - d_meson.SetMass(p4CH.m()); - d_meson.SetHelicity(interaction.primary_helicity); - -} - -double CharmHadronization::FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const { - if (secondary == siren::dataclasses::Particle::ParticleType::D0 || secondary == siren::dataclasses::Particle::ParticleType::D0Bar) { - return 0.6; - } else if (secondary == siren::dataclasses::Particle::ParticleType::DPlus || secondary == siren::dataclasses::Particle::ParticleType::DMinus) { - return 0.23; - } // D_s and Lambda^+ not yet implemented - return 0; -} - -} // namespace interactions -} // namespace siren \ No newline at end of file diff --git a/projects/interactions/private/pybindings/CharmDISFromSpline.h b/projects/interactions/private/pybindings/CharmDISFromSpline.h deleted file mode 100644 index e7349dc96..000000000 --- a/projects/interactions/private/pybindings/CharmDISFromSpline.h +++ /dev/null @@ -1,88 +0,0 @@ -#include -#include -#include - -#include -#include -#include - -#include "../../public/SIREN/interactions/CrossSection.h" -#include "../../public/SIREN/interactions/CharmDISFromSpline.h" -#include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" -#include "../../../dataclasses/public/SIREN/dataclasses/InteractionRecord.h" -#include "../../../dataclasses/public/SIREN/dataclasses/InteractionSignature.h" -#include "../../../geometry/public/SIREN/geometry/Geometry.h" -#include "../../../utilities/public/SIREN/utilities/Random.h" - -void register_CharmDISFromSpline(pybind11::module_ & m) { - using namespace pybind11; - using namespace siren::interactions; - - class_, CrossSection> charmdisfromspline(m, "CharmDISFromSpline"); - - charmdisfromspline - - .def(init<>()) - .def(init, std::vector, int, double, double, std::set, std::set, std::string>(), - arg("total_xs_data"), - arg("differential_xs_data"), - arg("interaction"), - arg("target_mass"), - arg("minimum_Q2"), - arg("primary_types"), - arg("target_types"), - arg("units") = std::string("cm")) - .def(init, std::vector, int, double, double, std::vector, std::vector, std::string>(), - arg("total_xs_data"), - arg("differential_xs_data"), - arg("interaction"), - arg("target_mass"), - arg("minimum_Q2"), - arg("primary_types"), - arg("target_types"), - arg("units") = std::string("cm")) - .def(init, std::set, std::string>(), - arg("total_xs_filename"), - arg("differential_xs_filename"), - arg("interaction"), - arg("target_mass"), - arg("minimum_Q2"), - arg("primary_types"), - arg("target_types"), - arg("units") = std::string("cm")) - .def(init, std::set, std::string>(), - arg("total_xs_filename"), - arg("differential_xs_filename"), - arg("primary_types"), - arg("target_types"), - arg("units") = std::string("cm")) - .def(init, std::vector, std::string>(), - arg("total_xs_filename"), - arg("differential_xs_filename"), - arg("interaction"), - arg("target_mass"), - arg("minimum_Q2"), - arg("primary_types"), - arg("target_types"), - arg("units") = std::string("cm")) - .def(init, std::vector, std::string>(), - arg("total_xs_filename"), - arg("differential_xs_filename"), - arg("primary_types"), - arg("target_types"), - arg("units") = std::string("cm")) - .def(self == self) - .def("SetInteractionType",&CharmDISFromSpline::SetInteractionType) - .def("TotalCrossSection",overload_cast(&CharmDISFromSpline::TotalCrossSection, const_)) - .def("TotalCrossSection",overload_cast(&CharmDISFromSpline::TotalCrossSection, const_)) - .def("DifferentialCrossSection",overload_cast(&CharmDISFromSpline::DifferentialCrossSection, const_)) - .def("DifferentialCrossSection",overload_cast(&CharmDISFromSpline::DifferentialCrossSection, const_)) - .def("InteractionThreshold",&CharmDISFromSpline::InteractionThreshold) - .def("GetPossibleTargets",&CharmDISFromSpline::GetPossibleTargets) - .def("GetPossibleTargetsFromPrimary",&CharmDISFromSpline::GetPossibleTargetsFromPrimary) - .def("GetPossiblePrimaries",&CharmDISFromSpline::GetPossiblePrimaries) - .def("GetPossibleSignatures",&CharmDISFromSpline::GetPossibleSignatures) - .def("GetPossibleSignaturesFromParents",&CharmDISFromSpline::GetPossibleSignaturesFromParents) - .def("FinalStateProbability",&CharmDISFromSpline::FinalStateProbability); -} - diff --git a/projects/interactions/private/pybindings/CharmHadronization.h b/projects/interactions/private/pybindings/CharmHadronization.h deleted file mode 100644 index c0c7c7788..000000000 --- a/projects/interactions/private/pybindings/CharmHadronization.h +++ /dev/null @@ -1,38 +0,0 @@ -#include -#include -#include - -#include -#include -#include - -#include "../../public/SIREN/interactions/Hadronization.h" -#include "../../public/SIREN/interactions/CharmHadronization.h" - - -#include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" -#include "../../../dataclasses/public/SIREN/dataclasses/InteractionRecord.h" -#include "../../../dataclasses/public/SIREN/dataclasses/InteractionSignature.h" -#include "../../../geometry/public/SIREN/geometry/Geometry.h" -#include "../../../utilities/public/SIREN/utilities/Random.h" - -using namespace pybind11; -using namespace siren::interactions; - - -void register_CharmHadronization(pybind11::module_ & m) { - using namespace pybind11; - using namespace siren::interactions; - - class_, Hadronization> charmhadronization(m, "CharmHadronization"); - - charmhadronization - - .def(init<>()) - .def(self == self) - .def("SampleFinalState",&CharmHadronization::SampleFinalState) - .def("GetPossibleSignatures",&CharmHadronization::GetPossibleSignatures) - .def("GetPossibleSignaturesFromParent",&CharmHadronization::GetPossibleSignaturesFromParent) - .def("FragmentationFraction",&CharmHadronization::FragmentationFraction); - ; -} diff --git a/projects/interactions/private/pybindings/DMesonELoss.h b/projects/interactions/private/pybindings/DMesonELoss.h index 427633c4e..3956f0bd5 100644 --- a/projects/interactions/private/pybindings/DMesonELoss.h +++ b/projects/interactions/private/pybindings/DMesonELoss.h @@ -7,7 +7,6 @@ #include #include "../../public/SIREN/interactions/CrossSection.h" -#include "../../public/SIREN/interactions/CharmDISFromSpline.h" #include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" #include "../../../dataclasses/public/SIREN/dataclasses/InteractionRecord.h" #include "../../../dataclasses/public/SIREN/dataclasses/InteractionSignature.h" diff --git a/projects/interactions/private/pybindings/interactions.cxx b/projects/interactions/private/pybindings/interactions.cxx index ccc07cf63..b333daf91 100644 --- a/projects/interactions/private/pybindings/interactions.cxx +++ b/projects/interactions/private/pybindings/interactions.cxx @@ -6,14 +6,12 @@ #include "../../public/SIREN/interactions/NeutrissimoDecay.h" #include "../../public/SIREN/interactions/InteractionCollection.h" #include "../../public/SIREN/interactions/DISFromSpline.h" -#include "../../public/SIREN/interactions/CharmDISFromSpline.h" #include "../../public/SIREN/interactions/QuarkDISFromSpline.h" #include "../../public/SIREN/interactions/HNLFromSpline.h" #include "../../public/SIREN/interactions/DipoleFromTable.h" #include "../../public/SIREN/interactions/DarkNewsCrossSection.h" #include "../../public/SIREN/interactions/DarkNewsDecay.h" #include "../../public/SIREN/interactions/Hadronization.h" -#include "../../public/SIREN/interactions/CharmHadronization.h" #include "../../public/SIREN/interactions/CharmMesonDecay.h" #include "../../public/SIREN/interactions/CharmMesonDecay3Body.h" #include "../../public/SIREN/interactions/DMesonELoss.h" @@ -25,7 +23,6 @@ #include "./DarkNewsCrossSection.h" #include "./DarkNewsDecay.h" #include "./DISFromSpline.h" -#include "./CharmDISFromSpline.h" #include "./QuarkDISFromSpline.h" #include "./HNLFromSpline.h" #include "./Decay.h" @@ -33,7 +30,6 @@ #include "./InteractionCollection.h" #include "./DummyCrossSection.h" #include "./Hadronization.h" -#include "./CharmHadronization.h" #include "./CharmMesonDecay.h" #include "./CharmMesonDecay3Body.h" #include "./DMesonELoss.h" @@ -56,7 +52,6 @@ PYBIND11_MODULE(interactions,m) { register_Decay(m); register_Hadronization(m); - register_CharmHadronization(m); register_CharmMesonDecay(m); register_CharmMesonDecay3Body(m); register_DMesonELoss(m); @@ -65,7 +60,6 @@ PYBIND11_MODULE(interactions,m) { register_DarkNewsCrossSection(m); register_DarkNewsDecay(m); register_DISFromSpline(m); - register_CharmDISFromSpline(m); register_QuarkDISFromSpline(m); register_HNLFromSpline(m); register_NeutrissimoDecay(m); diff --git a/projects/interactions/public/SIREN/interactions/CharmDISFromSpline.h b/projects/interactions/public/SIREN/interactions/CharmDISFromSpline.h deleted file mode 100644 index 102ce4725..000000000 --- a/projects/interactions/public/SIREN/interactions/CharmDISFromSpline.h +++ /dev/null @@ -1,166 +0,0 @@ -#pragma once -#ifndef SIREN_CharmDISFromSpline_H -#define SIREN_CharmDISFromSpline_H - -#include // for set -#include // for map -#include -#include // for vector -#include // for uint32_t -#include // for pair -#include -#include // for runtime... - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "SIREN/interactions/CrossSection.h" // for CrossSe... -#include "SIREN/dataclasses/InteractionSignature.h" // for Interac... -#include "SIREN/dataclasses/Particle.h" // for Particle - -namespace siren { namespace dataclasses { class InteractionRecord; } } -namespace siren { namespace utilities { class SIREN_random; } } - -namespace siren { -namespace interactions { - -class CharmDISFromSpline : public CrossSection { -friend cereal::access; -private: - photospline::splinetable<> differential_cross_section_; - photospline::splinetable<> total_cross_section_; - - std::vector signatures_; - std::set primary_types_; - std::set target_types_; - std::map> targets_by_primary_types_; - std::map, std::vector> signatures_by_parent_types_; - - int interaction_type_; - double target_mass_; - double minimum_Q2_; - - double unit; - -public: - CharmDISFromSpline(); - CharmDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minumum_Q2, std::set primary_types, std::set target_types, std::string units = "cm"); - CharmDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minumum_Q2, std::vector primary_types, std::vector target_types, std::string units = "cm"); - CharmDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minumum_Q2, std::set primary_types, std::set target_types, std::string units = "cm"); - CharmDISFromSpline(std::string differential_filename, std::string total_filename, std::set primary_types, std::set target_types, std::string units = "cm"); - CharmDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minumum_Q2, std::vector primary_types, std::vector target_types, std::string units = "cm"); - CharmDISFromSpline(std::string differential_filename, std::string total_filename, std::vector primary_types, std::vector target_types, std::string units = "cm"); - - void SetUnits(std::string units); - // this might be integrated later? could also make another initialization method - // problem with current implementation is that EM is not supported b/c at initialization we assume int = 1 - // this sets the isoscalar target mass - void SetInteractionType(int interaction); - - virtual bool equal(CrossSection const & other) const override; - - double TotalCrossSection(dataclasses::InteractionRecord const &) const override; - double TotalCrossSection(siren::dataclasses::ParticleType primary, double energy) const; - double DifferentialCrossSection(dataclasses::InteractionRecord const &) const override; - double DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2=std::numeric_limits::quiet_NaN()) const; - double InteractionThreshold(dataclasses::InteractionRecord const &) const override; - void SampleFinalState(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr random) const override; - - std::vector GetPossibleTargets() const override; - std::vector GetPossibleTargetsFromPrimary(siren::dataclasses::ParticleType primary_type) const override; - std::vector GetPossiblePrimaries() const override; - std::vector GetPossibleSignatures() const override; - std::vector GetPossibleSignaturesFromParents(siren::dataclasses::ParticleType primary_type, siren::dataclasses::ParticleType target_type) const override; - - virtual double FinalStateProbability(dataclasses::InteractionRecord const & record) const override; - - void LoadFromFile(std::string differential_filename, std::string total_filename); - void LoadFromMemory(std::vector & differential_data, std::vector & total_data); - - double GetMinimumQ2() const {return minimum_Q2_;}; - double GetTargetMass() const {return target_mass_;}; - int GetInteractionType() const {return interaction_type_;}; - - static double GetLeptonMass(siren::dataclasses::ParticleType lepton_type); - -public: - virtual std::vector DensityVariables() const override; - template - void save(Archive & archive, std::uint32_t const version) const { - if(version == 0) { - splinetable_buffer buf; - buf.size = 0; - auto result_obj = differential_cross_section_.write_fits_mem(); - buf.data = result_obj.first; - buf.size = result_obj.second; - - std::vector diff_blob; - diff_blob.resize(buf.size); - std::copy((char*)buf.data, (char*)buf.data + buf.size, &diff_blob[0]); - - archive(::cereal::make_nvp("DifferentialCrossSectionSpline", diff_blob)); - - buf.size = 0; - result_obj = total_cross_section_.write_fits_mem(); - buf.data = result_obj.first; - buf.size = result_obj.second; - - std::vector total_blob; - total_blob.resize(buf.size); - std::copy((char*)buf.data, (char*)buf.data + buf.size, &total_blob[0]); - - archive(::cereal::make_nvp("TotalCrossSectionSpline", total_blob)); - archive(::cereal::make_nvp("PrimaryTypes", primary_types_)); - archive(::cereal::make_nvp("TargetTypes", target_types_)); - archive(::cereal::make_nvp("InteractionType", interaction_type_)); - archive(::cereal::make_nvp("TargetMass", target_mass_)); - archive(::cereal::make_nvp("MinimumQ2", minimum_Q2_)); - archive(::cereal::make_nvp("Unit", unit)); - archive(cereal::virtual_base_class(this)); - } else { - throw std::runtime_error("CharmDISFromSpline only supports version <= 0!"); - } - } - template - void load(Archive & archive, std::uint32_t version) { - if(version == 0) { - std::vector differential_data; - std::vector total_data; - archive(::cereal::make_nvp("DifferentialCrossSectionSpline", differential_data)); - archive(::cereal::make_nvp("TotalCrossSectionSpline", total_data)); - archive(::cereal::make_nvp("PrimaryTypes", primary_types_)); - archive(::cereal::make_nvp("TargetTypes", target_types_)); - archive(::cereal::make_nvp("InteractionType", interaction_type_)); - archive(::cereal::make_nvp("TargetMass", target_mass_)); - archive(::cereal::make_nvp("MinimumQ2", minimum_Q2_)); - archive(::cereal::make_nvp("Unit", unit)); - archive(cereal::virtual_base_class(this)); - LoadFromMemory(differential_data, total_data); - InitializeSignatures(); - } else { - throw std::runtime_error("CharmDISFromSpline only supports version <= 0!"); - } - } -private: - void ReadParamsFromSplineTable(); - void InitializeSignatures(); -}; - -} // namespace interactions -} // namespace siren - -CEREAL_CLASS_VERSION(siren::interactions::CharmDISFromSpline, 0); -CEREAL_REGISTER_TYPE(siren::interactions::CharmDISFromSpline); -CEREAL_REGISTER_POLYMORPHIC_RELATION(siren::interactions::CrossSection, siren::interactions::CharmDISFromSpline); - -#endif // SIREN_CharmDISFromSpline_H diff --git a/projects/interactions/public/SIREN/interactions/CharmHadronization.h b/projects/interactions/public/SIREN/interactions/CharmHadronization.h deleted file mode 100644 index e81cfa4bb..000000000 --- a/projects/interactions/public/SIREN/interactions/CharmHadronization.h +++ /dev/null @@ -1,93 +0,0 @@ -#pragma once -#ifndef SIREN_CharmHadronization_H -#define SIREN_CharmHadronization_H - -#include // for shared_ptr -#include // for string -#include // for vector -#include // for uint32_t - -#include -#include -#include -#include -#include -#include - -#include "SIREN/interactions/Hadronization.h" // for Hadronization -#include "SIREN/dataclasses/Particle.h" // for Particle -#include "SIREN/dataclasses/InteractionSignature.h" // for InteractionSignature -#include "SIREN/dataclasses/InteractionRecord.h" // for InteractionSignature - -#include "SIREN/utilities/Random.h" // for SIREN_random -#include "SIREN/geometry/Geometry.h" -#include "SIREN/utilities/Constants.h" // for electronMass -#include "SIREN/utilities/Interpolator.h" -#include "SIREN/utilities/Integration.h" - - - - -namespace siren { namespace dataclasses { class InteractionRecord; } } -namespace siren { namespace dataclasses { struct InteractionSignature; } } -namespace siren { namespace utilities { class SIREN_random; } } - -namespace siren { -namespace interactions { - -class CharmHadronization : public Hadronization { -friend cereal::access; -private: - const std::set primary_types = {siren::dataclasses::Particle::ParticleType::Charm, siren::dataclasses::Particle::ParticleType::CharmBar}; - // z pdf setting should be enabled in the future, for now we hard code the Peterson function - double fragmentation_integral = 0; // for storing the integrated unnormed pdf - void normalize_pdf(); // for normalizing pdf and stroing integral, to be called at initialization - - siren::utilities::Interpolator1D inverseCdfTable; -public: - - CharmHadronization(); - - // virtual pybind11::object get_self(); - - virtual bool equal(Hadronization const & other) const override; - - void SampleFinalState(dataclasses::CrossSectionDistributionRecord & interaction, std::shared_ptr random) const override; - - virtual std::vector GetPossibleSignatures() const override; // Requires python-side implementation - virtual std::vector GetPossibleSignaturesFromParent(siren::dataclasses::Particle::ParticleType primary) const override; // Requires python-side implementation - - double FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const override; - - double sample_pdf(double z) const; - void compute_cdf(); - static double getHadronMass(siren::dataclasses::ParticleType hadron_type); - -public: - template - void save(Archive & archive, std::uint32_t const version) const { - if(version == 0) { - archive(::cereal::make_nvp("Hadronization", cereal::virtual_base_class(this))); - } else { - throw std::runtime_error("CharmHadronization only supports version <= 0!"); - } - } - template - void load_and_construct(Archive & archive, cereal::construct & construct, std::uint32_t version) { - if(version == 0) { - archive(::cereal::make_nvp("Hadronization", cereal::virtual_base_class(construct.ptr()))); - } else { - throw std::runtime_error("CharmHadronization only supports version <= 0!"); - } - } - -}; - -} // namespace interactions -} // namespace siren - -CEREAL_CLASS_VERSION(siren::interactions::CharmHadronization, 0); -CEREAL_REGISTER_TYPE(siren::interactions::CharmHadronization); -CEREAL_REGISTER_POLYMORPHIC_RELATION(siren::interactions::Hadronization, siren::interactions::CharmHadronization); - -#endif // SIREN_CharmHadronization_H diff --git a/resources/Examples/DMesonExample/DIS_D.py b/resources/Examples/DMesonExample/DIS_D.py deleted file mode 100644 index c89d25b00..000000000 --- a/resources/Examples/DMesonExample/DIS_D.py +++ /dev/null @@ -1,151 +0,0 @@ -import os -import numpy as np -import siren -from siren.SIREN_Controller import SIREN_Controller -import nuflux - -# Number of events to inject -events_to_inject = 10000 - -# Expeirment to run -experiment = "IceCube" - -# physical flux model to use -physical_flux = "atmos" - -# Define the controller -controller = SIREN_Controller(events_to_inject, experiment, seed = 1) - -# Particle to inject -primary_type = siren.dataclasses.Particle.ParticleType.NuMu - -xs_option = "" # current choices are the empty string and cutoff-"" -xsfiledir = "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/miaochenjin/CharmXS/xsec_splines/M_Muon-{}105MeV".format(xs_option) - -if xs_option == "": - spline_option = "M_Muon-105MeV" # this is to account for the fact that I put the output names of this particular spline incorrectly -else: - spline_option = "" - -# Cross Section Model -target_type = siren.dataclasses.Particle.ParticleType.Nucleon - -DIS_xs = siren.interactions.CharmDISFromSpline( - os.path.join(xsfiledir, "{}dsdxdynu-N-cc-HERAPDF15LO_EIG_central.fits".format(spline_option)), - os.path.join(xsfiledir, "{}sigmanu-N-cc-HERAPDF15LO_EIG_central.fits".format(spline_option)), - [primary_type], - [target_type], "m" -) - -primary_xs = siren.interactions.InteractionCollection(primary_type, [DIS_xs]) -controller.SetInteractions(primary_xs) - -# Primary distributions -primary_injection_distributions = {} -primary_physical_distributions = {} - -mass_dist = siren.distributions.PrimaryMass(0) -primary_injection_distributions["mass"] = mass_dist -primary_physical_distributions["mass"] = mass_dist - -# energy distribution -edist = siren.distributions.PowerLaw(2, 1e4, 1e10) -primary_injection_distributions["energy"] = edist - -# make an atmospheric flux -flux = nuflux.makeFlux('honda2006') -nu_type=nuflux.NuMu -erange = np.logspace(2,6,100) -erange_atmo = np.logspace(2,6,100) -cosrange = np.linspace(0,1,100) -atmo_flux_tables = {} -particle = nuflux.NuMu -siren_key = siren.dataclasses.Particle.ParticleType(int(particle)) -atmo_flux_tables[siren_key] = np.zeros(len(erange)) -for i,e in enumerate(erange): - f = flux.getFlux(particle,e,cosrange) - atmo_flux_tables[siren_key][i] += 0.01*np.sum(f) * 1e4 * 2 * np.pi - -# this is for weighting the events to the astrophysical flux -if physical_flux == "astro": - edist_astro = siren.distributions.PowerLaw(2, 1e4, 1e10) - norm = 1e-18 * 1e4 * 4 * np.pi # GeV^-1 m^-2 s^-1 - edist_astro.SetNormalizationAtEnergy(norm,1e5) - primary_physical_distributions["energy"] = edist_astro -elif physical_flux == "atmos": - edist_atmo = siren.distributions.TabulatedFluxDistribution(erange_atmo,atmo_flux_tables[primary_type],True) - primary_physical_distributions["energy"] = edist_atmo -else: - primary_injection_distributions["energy"] = edist - - -# direction distribution -direction_distribution = siren.distributions.IsotropicDirection() -primary_injection_distributions["direction"] = direction_distribution -primary_physical_distributions["direction"] = direction_distribution - -# position distribution -muon_range_func = siren.distributions.LeptonDepthFunction() -position_distribution = siren.distributions.ColumnDepthPositionDistribution( - 600, 600.0, muon_range_func, set(controller.GetDetectorModelTargets()[0]) -) -primary_injection_distributions["position"] = position_distribution - -# SetProcesses -controller.SetProcesses( - primary_type, primary_injection_distributions, primary_physical_distributions -) - -def add_secondary_to_controller(controller, secondary_type, secondary_xsecs, secondary_decays = None): - if secondary_decays is not None: - secondary_collection = siren.interactions.InteractionCollection(secondary_type, [secondary_xsecs], [secondary_decays]) - else: - secondary_collection = siren.interactions.InteractionCollection(secondary_type, [secondary_xsecs]) - secondary_injection_process = siren.injection.SecondaryInjectionProcess() - secondary_physical_process = siren.injection.PhysicalProcess() - secondary_injection_process.primary_type = secondary_type - secondary_physical_process.primary_type = secondary_type - secondary_injection_process.AddSecondaryInjectionDistribution(siren.distributions.SecondaryPhysicalVertexDistribution()) - controller.secondary_injection_processes.append(secondary_injection_process) - controller.secondary_physical_processes.append(secondary_physical_process) - - return secondary_collection - -# # secondary interactions -charms = siren.dataclasses.Particle.ParticleType.Charm -DPlus = siren.dataclasses.Particle.ParticleType.DPlus -D0 = siren.dataclasses.Particle.ParticleType.D0 -charm_hadronization = siren.interactions.CharmHadronization() -DPlus_decay = siren.interactions.CharmMesonDecay(primary_type = DPlus) -D0_decay = siren.interactions.CharmMesonDecay(primary_type = D0) -D_energy_loss = siren.interactions.DMesonELoss() - -secondary_charm_collection = add_secondary_to_controller(controller, charms, charm_hadronization) -secondary_DPlus_collection = add_secondary_to_controller(controller, DPlus, D_energy_loss, DPlus_decay) -secondary_D0_collection = add_secondary_to_controller(controller, D0, D_energy_loss, D0_decay) - -# secondary_DPlus_collection = add_secondary_to_controller(controller, DPlus, DPlus_decay) -# secondary_D0_collection = add_secondary_to_controller(controller, D0, D0_decay) - -controller.SetInteractions(primary_xs, [secondary_charm_collection, secondary_D0_collection, secondary_DPlus_collection]) -# controller.SetInteractions(primary_xs, [secondary_charm_collection]) -# controller.SetInteractions(primary_xs, []) - -controller.Initialize() - -def stop(datum, i): - return False - -controller.SetInjectorStoppingCondition(stop) - -events = controller.GenerateEvents() - -print("finished generating events") - -outdir = "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/miaochenjin/DBSearch/SIREN_outputs" -expname = "0819_LE_debug_CharmHadron_atmos" -savedir = os.path.join(outdir, expname) - -os.makedirs(savedir, exist_ok=True) - -controller.SaveEvents("{}/{}_".format(savedir, expname)) From 3341768c5ffb6667cb5c60af264a2a1e40119856 Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Sat, 2 May 2026 21:49:34 -0400 Subject: [PATCH 29/93] QuarkDISFromSpline: per-primary D-meson selection + remove vestigial quark_type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three correctness fixes consolidated into one pass: (1) Bug #7 — Per-primary D-meson selection. The pre-fix code used a single instance-level quark_type_ member to choose {D0, D+} vs {D0Bar, D-} for *every* primary in the instance. For mixed-flavor instances (e.g. primary_types={NuMu, NuMuBar}) the wrong-parity primary received the wrong D-meson types, violating charm conservation in the emitted signature. Fixed by introducing a private static helper DTypesForPrimary(primary) that maps each neutrino primary to its correct D-meson set per particle/antiparticle parity. Applied per-iteration inside InitializeSignatures(). (2) Vestigial quark_type member + ctor argument removed. After (1), quark_type_ became write-only state. Dropped: the private member, the int quark_type parameter from all 4 ctor signatures, SetQuarkType() setter and pybind binding, and the QUARKTYPE spline-key read inside ReadParamsFromSplineTable. Cereal save/load layout is unaffected (quark_type_ was not serialized). The 2 ctors that don't take quark_type (single-spline-file, fall through to ReadParamsFromSplineTable) keep their existing signatures. (3) Bug #9 — Missing breaks in GetLeptonMass(). Cases 12 and 14 fell through to case 16; behavior was accidentally correct (all set lepton_mass = 0) but a latent footgun. Added the missing break;. Caller updates: - resources/examples/example1/DIS_IceCube_charm.py: drop quark_type positional from QuarkDISFromSpline ctor call. Hygiene: - Removed leftover debug std::cout instrumentation from earlier Bug #6 / #11 / sampling work in QuarkDISFromSpline.cxx (45 lines, mostly inside SampleFinalState and the cross-section evaluation paths). - Removed one stray std::cout warning from DMesonELoss.cxx (xsec=0 case; function still returns 0 silently as before). Verified: pip rebuild clean; 100-event DIS_IceCube_charm.py dry-run exit 0; mixed-primary regression test confirms NuMu->{D0, D+} and NuMuBar->{D0Bar, D-} per signature. --- projects/interactions/private/DMesonELoss.cxx | 4 - .../private/QuarkDISFromSpline.cxx | 98 +++++-------------- .../private/pybindings/QuarkDISFromSpline.h | 13 +-- .../SIREN/interactions/QuarkDISFromSpline.h | 11 +-- .../examples/example1/DIS_IceCube_charm.py | 1 - 5 files changed, 34 insertions(+), 93 deletions(-) diff --git a/projects/interactions/private/DMesonELoss.cxx b/projects/interactions/private/DMesonELoss.cxx index 2d61e2d63..4e95520bc 100644 --- a/projects/interactions/private/DMesonELoss.cxx +++ b/projects/interactions/private/DMesonELoss.cxx @@ -116,10 +116,6 @@ double DMesonELoss::TotalCrossSection(siren::dataclasses::Particle::ParticleType // current implementation uses only > 1PeV data double xsec = exp(1.891 + 0.205 * log_energy) - 2.157 + 1.264 * log_energy; - if (xsec == 0) { - std::cout << "DMesonELoss total xsec gives 0 prob!" << std::endl; - } - return xsec * mb_to_cm2; } diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 428483b63..9cfed5e23 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -72,32 +72,29 @@ QuarkDISFromSpline::QuarkDISFromSpline() { compute_cdf(); } -QuarkDISFromSpline::QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, int quark_type, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), quark_type_(quark_type), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { +QuarkDISFromSpline::QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { normalize_pdf(); compute_cdf(); LoadFromMemory(differential_data, total_data); SetInteractionType(interaction); - SetQuarkType(quark_type); InitializeSignatures(); SetUnits(units); } -QuarkDISFromSpline::QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, int quark_type, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction),quark_type_(quark_type), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { +QuarkDISFromSpline::QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { normalize_pdf(); compute_cdf(); LoadFromMemory(differential_data, total_data); SetInteractionType(interaction); - SetQuarkType(quark_type); InitializeSignatures(); SetUnits(units); } -QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, int quark_type, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), quark_type_(quark_type), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { +QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minimum_Q2, std::set primary_types, std::set target_types, std::string units) : primary_types_(primary_types), target_types_(target_types), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { normalize_pdf(); compute_cdf(); LoadFromFile(differential_filename, total_filename); SetInteractionType(interaction); - SetQuarkType(quark_type); InitializeSignatures(); SetUnits(units); } @@ -111,12 +108,11 @@ QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::s SetUnits(units); } -QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, int quark_type, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), quark_type_(quark_type), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { +QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minimum_Q2, std::vector primary_types, std::vector target_types, std::string units) : primary_types_(primary_types.begin(), primary_types.end()), target_types_(target_types.begin(), target_types.end()), interaction_type_(interaction), target_mass_(target_mass), minimum_Q2_(minimum_Q2) { normalize_pdf(); compute_cdf(); LoadFromFile(differential_filename, total_filename); SetInteractionType(interaction); - SetQuarkType(quark_type); InitializeSignatures(); SetUnits(units); } @@ -146,10 +142,6 @@ void QuarkDISFromSpline::SetInteractionType(int interaction) { interaction_type_ = interaction; } -void QuarkDISFromSpline::SetQuarkType(int q_type) { - quark_type_ = q_type; -} - bool QuarkDISFromSpline::equal(CrossSection const & other) const { const QuarkDISFromSpline* x = dynamic_cast(&other); // to do: include more features in the hadronization side to check equivalence @@ -213,8 +205,10 @@ double QuarkDISFromSpline::GetLeptonMass(siren::dataclasses::ParticleType lepton break; case 12: lepton_mass = 0; + break; case 14: lepton_mass = 0; + break; case 16: lepton_mass = 0; break; @@ -265,13 +259,10 @@ std::map QuarkDISFromSpline::getIndices(siren::dataclasses::In void QuarkDISFromSpline::ReadParamsFromSplineTable() { // returns true if successfully read target mass bool mass_good = differential_cross_section_.read_key("TARGETMASS", target_mass_); - if (mass_good) {std::cout << "read target mass!!" << std::endl;} // for debugging purposes // returns true if successfully read interaction type bool int_good = differential_cross_section_.read_key("INTERACTION", interaction_type_); // returns true if successfully read minimum Q2 bool q2_good = differential_cross_section_.read_key("Q2MIN", minimum_Q2_); - // returns true if successfully read quark type - bool qtype_good = differential_cross_section_.read_key("QUARKTYPE", quark_type_); if(!int_good) { @@ -279,10 +270,6 @@ void QuarkDISFromSpline::ReadParamsFromSplineTable() { interaction_type_ = 1; } - if (!qtype_good) { - quark_type_ = 1; // assume quark is produced - } - if(!q2_good) { // assume 1 GeV^2 minimum_Q2_ = 1; @@ -310,6 +297,22 @@ void QuarkDISFromSpline::ReadParamsFromSplineTable() { } } +std::set QuarkDISFromSpline::DTypesForPrimary(siren::dataclasses::ParticleType primary) { + if (primary == siren::dataclasses::ParticleType::NuE + || primary == siren::dataclasses::ParticleType::NuMu + || primary == siren::dataclasses::ParticleType::NuTau) { + return {siren::dataclasses::Particle::ParticleType::D0, + siren::dataclasses::Particle::ParticleType::DPlus}; + } else if (primary == siren::dataclasses::ParticleType::NuEBar + || primary == siren::dataclasses::ParticleType::NuMuBar + || primary == siren::dataclasses::ParticleType::NuTauBar) { + return {siren::dataclasses::Particle::ParticleType::D0Bar, + siren::dataclasses::Particle::ParticleType::DMinus}; + } else { + throw std::runtime_error("DTypesForPrimary: Unknown neutrino primary type!"); + } +} + void QuarkDISFromSpline::InitializeSignatures() { signatures_.clear(); for(auto primary_type : primary_types_) { @@ -347,16 +350,8 @@ void QuarkDISFromSpline::InitializeSignatures() { } // now push back the hadron product signature.secondary_types.push_back(siren::dataclasses::ParticleType::Hadrons); - // define the charmed meson types based on the quark type, now considering only D0 and D+ - if (quark_type_ == 1) { - D_types_ = {siren::dataclasses::Particle::ParticleType::D0, - siren::dataclasses::Particle::ParticleType::DPlus}; - } else { - D_types_ = {siren::dataclasses::Particle::ParticleType::D0Bar, - siren::dataclasses::Particle::ParticleType::DMinus}; - } - // push back the meson type - for (auto meson_type : D_types_) { + std::set d_types_local = DTypesForPrimary(primary_type); + for (auto meson_type : d_types_local) { dataclasses::InteractionSignature full_signature = signature; full_signature.secondary_types.push_back(meson_type); // and finally set the target type and push back the entire signature as well as sig by target @@ -379,7 +374,6 @@ void QuarkDISFromSpline::normalize_pdf() { }; fragmentation_integral = siren::utilities::rombergIntegrate(integrand, 0.001, 0.999); } else { - std::cout << "Something is wrong... you already computed the normalization" << std::endl; return; } } @@ -445,7 +439,6 @@ double QuarkDISFromSpline::TotalCrossSection(dataclasses::InteractionRecord cons primary_energy = interaction.primary_momentum[0]; // if we are below threshold, return 0 if(primary_energy < InteractionThreshold(interaction)) { - std::cout << "DIS::interaction threshold not satisfied" << std::endl; return 0; } double total_xs = TotalCrossSection(primary_type, primary_energy); @@ -479,7 +472,6 @@ double QuarkDISFromSpline::TotalCrossSection(siren::dataclasses::ParticleType pr double log_xs = total_cross_section_.ndsplineeval(&log_energy, ¢er, 0); if (std::pow(10.0, log_xs) == 0) { - std::cout << "DIS::cross section evaluated to 0" << std::endl; } return unit * std::pow(10.0, log_xs); @@ -519,7 +511,6 @@ double QuarkDISFromSpline::DifferentialCrossSection(dataclasses::InteractionReco if (Q2 < minimum_Q2_ || !kinematicallyAllowed(x, y, primary_energy, target_mass_, lepton_mass) || !differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { - // std::cout << "weighting: revert back to saved x and y" << std::endl; double E1_lab = interaction.interaction_parameters.at("energy"); double E2_lab = p2.e(); x = interaction.interaction_parameters.at("bjorken_x"); @@ -534,14 +525,12 @@ double QuarkDISFromSpline::DifferentialCrossSection(double energy, double x, dou // check preconditions if(log_energy < differential_cross_section_.lower_extent(0) || log_energy>differential_cross_section_.upper_extent(0)) - {std::cout << "Diff xsec: not in bounds" << std::endl; + { return 0.0;} if(x <= 0 || x >= 1) { - std::cout << "x is out of bounds with x = " << x << std::endl; return 0.0; } if(y <= 0 || y >= 1){ - std::cout << "y is out of bounds with x = " << y << std::endl; return 0.0; } @@ -549,25 +538,20 @@ double QuarkDISFromSpline::DifferentialCrossSection(double energy, double x, dou Q2 = 2.0 * energy * target_mass_ * x * y; } if(Q2 < minimum_Q2_) { - std::cout << "Q2 is smaller than minimum Q2 with " << Q2 << " < " << minimum_Q2_ << std::endl; return 0; } // cross section not calculated, assumed to be zero if(!kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) { - std::cout << "not kinematically allowed!" << std::endl; return 0; } std::array coordinates{{log_energy, log10(x), log10(y)}}; std::array centers; if(!differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { - std::cout << "search centers failed!" << std::endl; return 0; } double result = pow(10., differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0)); assert(result >= 0); if (std::isinf(result)) { - std::cout << "energy, x, y, Q2 are " << energy << " " << x << " " << y << " " << Q2 << " " << std::endl; - std::cout << "spline value read is " << differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0) << std::endl; } return unit * result; } @@ -579,7 +563,6 @@ double QuarkDISFromSpline::InteractionThreshold(dataclasses::InteractionRecord c void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { // first obtain the indices from secondaries - // std::cout << "in sample final state" << std::endl; std::map secondary_indices = getIndices(record.signature); unsigned int lepton_index = secondary_indices["lepton"]; unsigned int hadron_index = secondary_indices["hadron"]; @@ -591,7 +574,6 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR throw std::runtime_error("I expected 3 dimensions in the cross section spline, but got " + std::to_string(differential_cross_section_.get_ndim()) +". Maybe your fits file doesn't have the right 'INTERACTION' key?"); } rk::P4 p1(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); - // std::cout << "quark::sampleFinalState : primary momentum is read to be " << p1 << std::endl; rk::P4 p2(geom3::Vector3(0, 0, 0), target_mass_); // we assume that: @@ -725,7 +707,6 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR if(accept) { kin_vars = test_kin_vars; cross_section = test_cross_section; - // std::cout << "trial Q is" << trialQ << std::endl; } } @@ -757,7 +738,6 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); if (pqx_lab>momq_lab){ - // std::cout << "triggered on " << momq_lab << " and " << pqx_lab << std::endl; //scale down E1_lab /= 10; E2_lab /= 10; @@ -771,12 +751,10 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR continue; } pqy_lab = std::sqrt((momq_lab + pqx_lab) * (momq_lab - pqx_lab)); - // std::cout << "finished with " << iteration << " iterations and " << momq_lab << " and " << pqx_lab << std::endl; break; } // //scale back if (iteration > 0) { - // std::cout << "scaling back with " << pow(10.0, iteration); E1_lab *= pow(10.0, iteration); E2_lab *= pow(10.0, iteration); p1_lab_x *= pow(10.0, iteration); @@ -784,7 +762,6 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR p1_lab_z *= pow(10.0, iteration); m1 *= pow(10.0, iteration); m3 *= pow(10.0, iteration); - // std::cout << "and finished with " << momq_lab << " and " << pqx_lab << std::endl; } // pqy_lab = 0; } else {pqy_lab = std::sqrt(momq_lab*momq_lab - pqx_lab *pqx_lab);} @@ -874,7 +851,6 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR // compute the energy and 3-momentum of the virtual charm - // std::cout << "the virtual charm off-shell mass is " << p4.m() << std::endl; double p3c = std::sqrt(std::pow(p4.px(), 2) + std::pow(p4.py(), 2) + std::pow(p4.pz(), 2)); double Ec = p4.e(); //energy of primary charm double mCH = getHadronMass(record.signature.secondary_types[meson_index]); // obtain charmed hadron mass @@ -894,8 +870,6 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR do { sampling += 1; if (sampling > max_sampling) { - std::cout << "energy of the charm is " << Ec << " and momentum is " << p3c << std::endl; - std::cout << "desired mass of hadron is " << mCH << std::endl; // throw(siren::utilities::InjectionFailure("Failed to sample hadronization!")); break; } @@ -911,26 +885,16 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR // new attempt of using the isoscalar mass as the remnant hadronic shower mass double mX = target_mass_; double Mc = p4.m(); - // std::cout << "using remnant mass " << mX << std::endl; - // std::cout << "invariant charm mass and its energy is " << Mc << ", " << p4.e() << std::endl; - // std::cout << "target sampled D meson energy is " << ECH << std::endl; - // std::cout << "and the fraction of momentum is sampled to be " << z << std::endl; //compute the energies in the charm rest frame double E_CH_c = (std::pow(Mc, 2) - std::pow(mX, 2) + std::pow(mCH, 2)) / (2 * Mc); - // std::cout << "energy of charm in rest frame is " << E_CH_c << std::endl; double p_c = std::sqrt((std::pow(Mc, 2) - std::pow(mCH + mX, 2)) * (std::pow(Mc, 2) - std::pow(mCH - mX, 2))) / (2 * Mc); - // std::cout << "momentum in charm rest frame is " << p_c << std::endl; // compute the lorentz boost parameters double gamma = p4.gamma(); double beta = p4.beta(); - // std::cout << "beta and gamma parameters are " << beta << ", " << gamma << std::endl; // using the lab frame fragmented energy and the double cosTheta = std::max(std::min(((ECH - gamma * E_CH_c)/(gamma * beta * p_c)), 1.), -1.); - // std::cout << "cosine of theta in charm frame is " << cosTheta << std::endl; - // std::cout << "without cutting, the number is " << (ECH - gamma * E_CH_c)/(gamma * beta * p_c) << std::endl; // now compute the momentum vectors in the rest frame double sinTheta = std::sin(std::acos(cosTheta)); - // std::cout << "and sine of theta is computed to be " << sinTheta << std::endl; rk::P4 p4CH_c(p_c * geom3::Vector3(cosTheta, sinTheta, 0), mCH); rk::P4 p4X_c(p_c * geom3::Vector3(-cosTheta, -sinTheta, 0), mX); // these all assume boost direction is charm direction. Now we should rotate back to charm lab momentum direction @@ -951,9 +915,6 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR rk::P4 p4X = p4X_c.boost(boost_from_crest_to_lab); rk::P4 p4CH = p4CH_c.boost(boost_from_crest_to_lab); - // std::cout << "computed remnant mass and energy is " << p4X.m() << ", " << p4X.e() << std::endl; - // std::cout << "and computed D mass and energy is " << p4CH.m() << ", " << p4CH.e() << std::endl; - // std::cout << "target sampled D meson energy is " << ECH << std::endl; // now we proceed to saving the final state kinematics @@ -961,21 +922,16 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[lepton_index]; siren::dataclasses::SecondaryParticleRecord & hadron = secondaries[hadron_index]; siren::dataclasses::SecondaryParticleRecord & meson = secondaries[meson_index]; - // std::cout << "QuarkDIS::SampleFInalState : the indices are: " << lepton_index << hadron_index<< meson_index << std::endl; lepton.SetFourMomentum({p3.e(), p3.px(), p3.py(), p3.pz()}); - // std::cout << "setting lepton mass with lepton momentum " << p3 << std::endl; lepton.SetMass(p3.m()); lepton.SetHelicity(record.primary_helicity); hadron.SetFourMomentum({p4X.e(), p4X.px(), p4X.py(), p4X.pz()}); - // std::cout << "setting hadron mass with hadron momentum " << p4X << std::endl; hadron.SetMass(p4X.m()); hadron.SetHelicity(record.target_helicity); meson.SetFourMomentum({p4CH.e(), p4CH.px(), p4CH.py(), p4CH.pz()}); - // std::cout << "setting meson mass with meson momentum " << p4CH << std::endl; meson.SetMass(p4CH.m()); meson.SetHelicity(record.target_helicity); // this needs working on - // std::cout << "finished sampling final state" << std::endl; */ } @@ -992,16 +948,12 @@ double QuarkDISFromSpline::FinalStateProbability(dataclasses::InteractionRecord // first compute the differential and total cross section double dxs = DifferentialCrossSection(interaction); // if (dxs == 0) { - // std::cout << "diff xsec gives 0" << std::endl; // } double txs = TotalCrossSection(interaction); // fragmentation fraction is now applied inside TotalCrossSection if(dxs == 0) { - std::cout << "diff xsec gives 0" << std::endl; return 0.0; } else { - // if (txs == 0) {std::cout << "wtf??? txs is 0 in final state prob" << txs << std::endl;} - if (std::isinf(dxs)) {std::cout << "dxs is inf in final state prob" << std::endl;} return dxs / txs; } } diff --git a/projects/interactions/private/pybindings/QuarkDISFromSpline.h b/projects/interactions/private/pybindings/QuarkDISFromSpline.h index 46e68053d..1764d4bb9 100644 --- a/projects/interactions/private/pybindings/QuarkDISFromSpline.h +++ b/projects/interactions/private/pybindings/QuarkDISFromSpline.h @@ -23,31 +23,28 @@ void register_QuarkDISFromSpline(pybind11::module_ & m) { quarkdisfromspline .def(init<>()) - .def(init, std::vector, int, int, double, double, std::set, std::set, std::string>(), + .def(init, std::vector, int, double, double, std::set, std::set, std::string>(), arg("total_xs_data"), arg("differential_xs_data"), arg("interaction"), - arg("quark_type"), arg("target_mass"), arg("minimum_Q2"), arg("primary_types"), arg("target_types"), arg("units") = std::string("cm")) - .def(init, std::vector, int, int, double, double, std::vector, std::vector, std::string>(), + .def(init, std::vector, int, double, double, std::vector, std::vector, std::string>(), arg("total_xs_data"), arg("differential_xs_data"), arg("interaction"), - arg("quark_type"), arg("target_mass"), arg("minimum_Q2"), arg("primary_types"), arg("target_types"), arg("units") = std::string("cm")) - .def(init, std::set, std::string>(), + .def(init, std::set, std::string>(), arg("total_xs_filename"), arg("differential_xs_filename"), arg("interaction"), - arg("quark_type"), arg("target_mass"), arg("minimum_Q2"), arg("primary_types"), @@ -59,11 +56,10 @@ void register_QuarkDISFromSpline(pybind11::module_ & m) { arg("primary_types"), arg("target_types"), arg("units") = std::string("cm")) - .def(init, std::vector, std::string>(), + .def(init, std::vector, std::string>(), arg("total_xs_filename"), arg("differential_xs_filename"), arg("interaction"), - arg("quark_type"), arg("target_mass"), arg("minimum_Q2"), arg("primary_types"), @@ -77,7 +73,6 @@ void register_QuarkDISFromSpline(pybind11::module_ & m) { arg("units") = std::string("cm")) .def(self == self) .def("SetInteractionType",&QuarkDISFromSpline::SetInteractionType) - .def("SetQuarkType",&QuarkDISFromSpline::SetQuarkType) .def("TotalCrossSection",overload_cast(&QuarkDISFromSpline::TotalCrossSection, const_)) .def("TotalCrossSection",overload_cast(&QuarkDISFromSpline::TotalCrossSection, const_)) .def("DifferentialCrossSection",overload_cast(&QuarkDISFromSpline::DifferentialCrossSection, const_)) diff --git a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h index 8a30eab86..e487ec3bd 100644 --- a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h +++ b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h @@ -51,7 +51,6 @@ friend cereal::access; // used by the DIS process int interaction_type_; - int quark_type_; double target_mass_; double minimum_Q2_; @@ -64,16 +63,15 @@ friend cereal::access; public: QuarkDISFromSpline(); - QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, int quark_type, double target_mass, double minumum_Q2, std::set primary_types, std::set target_types, std::string units = "cm"); - QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, int quark_type, double target_mass, double minumum_Q2, std::vector primary_types, std::vector target_types, std::string units = "cm"); - QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, int quark_type, double target_mass, double minumum_Q2, std::set primary_types, std::set target_types, std::string units = "cm"); + QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minumum_Q2, std::set primary_types, std::set target_types, std::string units = "cm"); + QuarkDISFromSpline(std::vector differential_data, std::vector total_data, int interaction, double target_mass, double minumum_Q2, std::vector primary_types, std::vector target_types, std::string units = "cm"); + QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minumum_Q2, std::set primary_types, std::set target_types, std::string units = "cm"); QuarkDISFromSpline(std::string differential_filename, std::string total_filename, std::set primary_types, std::set target_types, std::string units = "cm"); - QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, int quark_type, double target_mass, double minumum_Q2, std::vector primary_types, std::vector target_types, std::string units = "cm"); + QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minumum_Q2, std::vector primary_types, std::vector target_types, std::string units = "cm"); QuarkDISFromSpline(std::string differential_filename, std::string total_filename, std::vector primary_types, std::vector target_types, std::string units = "cm"); void SetUnits(std::string units); void SetInteractionType(int interaction); - void SetQuarkType(int q_type); virtual bool equal(CrossSection const & other) const override; @@ -172,6 +170,7 @@ friend cereal::access; private: void ReadParamsFromSplineTable(); void InitializeSignatures(); + static std::set DTypesForPrimary(siren::dataclasses::ParticleType primary); }; } // namespace interactions diff --git a/resources/examples/example1/DIS_IceCube_charm.py b/resources/examples/example1/DIS_IceCube_charm.py index 07cab9098..0b29895fd 100644 --- a/resources/examples/example1/DIS_IceCube_charm.py +++ b/resources/examples/example1/DIS_IceCube_charm.py @@ -83,7 +83,6 @@ def make_quark_dis_xs(pdf, target, current_type): os.path.join(SPLINES_DIR, f"dsdxdy_nu-N-{current_type}-charm-{pdf}.fits"), os.path.join(SPLINES_DIR, f"sigma_nu-N-{current_type}-charm-{pdf}.fits"), int(int_type), # interaction type: 1=CC, 2=NC - int(1), # quark type: 1=charm isoscalar_mass, 1, # min Q^2 [PRIMARY_TYPE], From 1110a2d2457cc520b38682f1a6f034eb96ea0fce Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Sat, 2 May 2026 21:49:44 -0400 Subject: [PATCH 30/93] Constants: swap D0Mass / DPlusMass to match PDG conventions (Bug #12) Constants::D0Mass had been holding 1.86962 GeV (a slightly stale D+ value) and Constants::DPlusMass had been holding 1.86484 GeV (the D0 PDG value). Both downstream consumers (CharmMesonDecay, CharmMesonDecay3Body) accessed these constants by name, so each D-meson decay was using the wrong mass for kinematics by ~0.3%. Fixed by swapping the literal values to match the names: D0Mass = 1.86484 // PDG D0 DPlusMass = 1.86966 // PDG D+ (also bumped from stale 1.86962) Also dropped two stale '(SWAPPED!)' annotations in CharmMesonDecay_TEST that were noting the bug -- the test was using Constants::D0Mass for a D0 decay test and is now correct as-is post-fix. --- projects/interactions/private/test/CharmMesonDecay_TEST.cxx | 4 ++-- projects/utilities/public/SIREN/utilities/Constants.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx index 682b97212..af249234a 100644 --- a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx +++ b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx @@ -224,7 +224,7 @@ TEST(Interpolator1D, DMesonDecayCDF) { TEST(CharmMesonDecay, DiffDecayWidthComparison) { // Check if the SIREN DifferentialDecayWidth matches our analytical formula - double mD = Constants::D0Mass; // 1.86962 (note: swapped with DPlus in Constants.h!) + double mD = Constants::D0Mass; // PDG D0 mass double mK = Constants::KminusMass; double F0CKM = 0.719; double alpha = 0.50; @@ -233,7 +233,7 @@ TEST(CharmMesonDecay, DiffDecayWidthComparison) { std::cout << "\nTest 2b: DifferentialDecayWidth comparison" << std::endl; std::cout << " Constants::D0Mass = " << Constants::D0Mass << " (PDG D0 = 1.86484, PDG D+ = 1.86966)" << std::endl; - std::cout << " Constants::DPlusMass = " << Constants::DPlusMass << " (SWAPPED!)" << std::endl; + std::cout << " Constants::DPlusMass = " << Constants::DPlusMass << std::endl; // Our analytical formula auto dGamma_analytical = [&](double Q2) -> double { diff --git a/projects/utilities/public/SIREN/utilities/Constants.h b/projects/utilities/public/SIREN/utilities/Constants.h index f01cb1567..49031fe7b 100644 --- a/projects/utilities/public/SIREN/utilities/Constants.h +++ b/projects/utilities/public/SIREN/utilities/Constants.h @@ -53,8 +53,8 @@ static const double EtaMass = 0.547862; // GeV static const double EtaPrimeMass = 0.95778; // GeV static const double RhoMass = 0.77526; // GeV static const double OmegaMass = 0.78266; // GeV -static const double D0Mass = 1.86962; // GeV -static const double DPlusMass = 1.86484; // GeV +static const double D0Mass = 1.86484; // GeV (PDG D0) +static const double DPlusMass = 1.86966; // GeV (PDG D+) static const double CharmMass = 1.27; // GeV // confusing units From d77526fab6def9759151390afa0c18b5e785d248 Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Wed, 8 Apr 2026 11:28:55 -0400 Subject: [PATCH 31/93] Add PythiaDISCrossSection and improve CharmMesonDecay Hard scattering: new PythiaDISCrossSection class wrapping Pythia8 for charm DIS (nu + N -> mu + c). Replaces QuarkDISFromSpline + CharmHadronization with a single class that performs the hard scatter and string fragmentation via Pythia, producing D mesons with physically correct kinematics. Non-charm CKM elements zeroed to force charm production. Spline-based cross sections retained for SIREN weighting. D meson decay: replaced 2-body cascade (D->K+W*, W*->mu+nu with form-factor CDF) with Pythia-style 3-body phase space sampling and V-A matrix element accept-reject (wtME = m_D * E_mu * (p_nu . p_K)). Added D->K*mu nu channel (K* mass 892 MeV) selected per-event based on PDG branching ratios alongside D->K mu nu. Restored physical branching ratios for all decay modes. Co-Authored-By: Claude Opus 4.6 --- .../public/SIREN/injection/Weighter.tcc | 18 +- .../interactions/private/CharmMesonDecay.cxx | 191 ++++++++++++------ .../SIREN/interactions/QuarkDISFromSpline.h | 1 + 3 files changed, 132 insertions(+), 78 deletions(-) diff --git a/projects/injection/public/SIREN/injection/Weighter.tcc b/projects/injection/public/SIREN/injection/Weighter.tcc index f153be6fa..4e3f76f20 100644 --- a/projects/injection/public/SIREN/injection/Weighter.tcc +++ b/projects/injection/public/SIREN/injection/Weighter.tcc @@ -121,12 +121,9 @@ double ProcessWeighter::InteractionProbability(std::tuple> const & xs_list = target_xs.second; double total_xs = 0.0; for(auto const & xs : xs_list) { - std::vector signatures = xs->GetPossibleSignaturesFromParents(record.signature.primary_type, target_xs.first); - for(auto const & signature : signatures) { - fake_record.signature = signature; - // Add total cross section - total_xs += xs->TotalCrossSection(fake_record); - } + fake_record.signature.primary_type = record.signature.primary_type; + fake_record.signature.target_type = target_xs.first; + total_xs += xs->TotalCrossSectionAllFinalStates(fake_record); } total_cross_sections.push_back(total_xs); } @@ -172,12 +169,9 @@ double ProcessWeighter::NormalizedPositionProbability(std::tuple> const & xs_list = target_xs.second; double total_xs = 0.0; for(auto const & xs : xs_list) { - std::vector signatures = xs->GetPossibleSignaturesFromParents(record.signature.primary_type, target_xs.first); - for(auto const & signature : signatures) { - fake_record.signature = signature; - // Add total cross section - total_xs += xs->TotalCrossSection(fake_record); - } + fake_record.signature.primary_type = record.signature.primary_type; + fake_record.signature.target_type = target_xs.first; + total_xs += xs->TotalCrossSectionAllFinalStates(fake_record); } total_cross_sections.push_back(total_xs); } diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx index ac1306f10..24298d8dc 100644 --- a/projects/interactions/private/CharmMesonDecay.cxx +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -156,13 +156,16 @@ double CharmMesonDecay::TotalDecayWidthForFinalState(dataclasses::InteractionRec std::set hadrons = {siren::dataclasses::Particle::ParticleType::Hadrons}; if (primary == siren::dataclasses::Particle::ParticleType::DPlus) { tau = 1040 * (1e-15); + // if (secondaries == k0_eplus_nue) {branching_ratio = .1607;} // e+ semileptonic mode according to pdg + // else if (secondaries == k0_muplus_numu) {branching_ratio = .176;} // mu+ anything according to pdg + // else if (secondaries == hadrons) {branching_ratio = (1 - .1607 - .176);} // everything else if (secondaries == k0_eplus_nue) {branching_ratio = .1607;} // e+ semileptonic mode according to pdg - else if (secondaries == k0_muplus_numu) {branching_ratio = .176;} // mu+ anything according to pdg + else if (secondaries == k0_muplus_numu) {branching_ratio = .176;} // mu+ anything according to pdg (K + K* combined) else if (secondaries == hadrons) {branching_ratio = (1 - .1607 - .176);} // everything else } else if (primary == siren::dataclasses::Particle::ParticleType::D0) { tau = 410.1 * (1e-15); if (secondaries == kminus_eplus_nue) {branching_ratio = .0649;} // e+ semileptonic mode according to pdg - else if (secondaries == kminus_muplus_numu) {branching_ratio = .067;} // mu+ anything according to pdg + else if (secondaries == kminus_muplus_numu) {branching_ratio = .067;} // mu+ anything according to pdg (K + K* combined) else if (secondaries == hadrons) {branching_ratio = (1 - .0649 - .067);} // everything else } else { @@ -211,6 +214,7 @@ std::vector CharmMesonDecay::GetPossibleSigna semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::KMinus; semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::EPlus; semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuE; + signatures.push_back(semilep_signature); semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::KMinus; semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::MuPlus; @@ -353,82 +357,137 @@ void CharmMesonDecay::SampleFinalStateHadronic(dataclasses::CrossSectionDistribu } void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { - // first handle hadronic decay separately, in the end we want to handle all decays in separate functions + // first handle hadronic decay separately dataclasses::InteractionSignature signature = record.signature; if (signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::Hadrons) { SampleFinalStateHadronic(record, random); return; } - // first obtain the constants needed for further computation from the signature - std::vector constants = FormFactorFromRecord(record); - double mD = particleMass(record.signature.primary_type); - double mK = particleMass(record.signature.secondary_types[0]); - // first sample a q^2 - double rand_value_for_Q2 = random->Uniform(0, 1); - double Q2 = inverseCdf(rand_value_for_Q2); + // ========================================================================= + // 3-body phase space sampling following Pythia's approach + // (ParticleDecays::threeBody in ParticleDecays.cc) + // + // D (m0) -> K (m1) + lepton (m2) + neutrino (m3) + // + // Phase space: sample m23 (lepton-neutrino invariant mass = sqrt(q^2)) + // flat in allowed range, accept-reject on phase space weight. + // Then apply V-A matrix element correction. + // ========================================================================= + + double mD = particleMass(record.signature.primary_type); // m0 + double mK_base = particleMass(record.signature.secondary_types[0]); // m1 (K mass from signature) + double ml = particleMass(record.signature.secondary_types[1]); // m2 + double mnu = 0.0; // m3 + + // Randomly choose between D->K l nu and D->K* l nu channels + // based on their relative branching ratios within the muonic mode. + // Pythia uses the same V-A matrix element (meMode=22) for both; + // only the hadron mass differs: K(494 MeV) vs K*(892 MeV). + double mKstar = 0.89166; // K*(892) mass in GeV + double fracK; // fraction of events that are D->K (vs D->K*) + if (record.signature.primary_type == siren::dataclasses::Particle::ParticleType::DPlus) { + // D+ -> K0bar mu+ nu: BR=8.74%, D+ -> K*0bar mu+ nu: BR=5.33% + fracK = 8.74 / (8.74 + 5.33); // ~0.621 + } else { + // D0 -> K- mu+ nu: BR=3.41%, D0 -> K*- mu+ nu: BR=2.17% + fracK = 3.41 / (3.41 + 2.17); // ~0.611 + } + double mK = (random->Uniform(0, 1) < fracK) ? mK_base : mKstar; - // now sample isotropically the "zenith" direction - double cosTheta = random->Uniform(-1, 1); - double sinTheta = std::sin(std::acos(cosTheta)); - // set the x axis to be the D direction + // D meson 4-momentum in lab frame + rk::P4 p4D_lab(geom3::Vector3(record.primary_momentum[1], + record.primary_momentum[2], + record.primary_momentum[3]), + record.primary_mass); geom3::UnitVector3 x_dir = geom3::UnitVector3::xAxis(); - //set the D direction in lab frame and compute its angle wrt the x axis - rk::P4 p4D_lab(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); - geom3::Vector3 p3D_lab = p4D_lab.momentum(); - geom3::UnitVector3 p3D_lab_dir = p3D_lab.direction(); - geom3::Rotation3 x_to_p3D_lab_rot = geom3::rotationBetween(x_dir, p3D_lab_dir); - // compute the momentum magnitude of the W and the K/pi - double EK = 0.5 * (pow(mD, 2) + pow(mK, 2) - Q2) / mD; // energy of Kaon - double PK = pow(pow(EK, 2) - pow(mK, 2), .5); // momentum magnitude of kaon in D rest frame - double PW = sqrt(Q2); // momentum magnitude of virtual W in D rest frame - // compute the 3 vectors of the W and the K/pi in the D rest frame, defined wrt x axis - rk::P4 p4K_Drest(PK * geom3::Vector3(cosTheta, sinTheta, 0), mK); - rk::P4 p4W_Drest(PK * geom3::Vector3(-cosTheta, -sinTheta, 0), PW); // invariant mass assigned to virtual W boson - - // rotate the momentum vectors so they are defined wrt to the D lab frame direction - p4K_Drest.rotate(x_to_p3D_lab_rot); - p4W_Drest.rotate(x_to_p3D_lab_rot); - // perform the random "azimuth" rotation - double phi = random->Uniform(0, 2 * M_PI); - geom3::Rotation3 azimuth_rand_rot(p3D_lab_dir, phi); - p4K_Drest.rotate(azimuth_rand_rot); - p4W_Drest.rotate(azimuth_rand_rot); - // finally, boost the 4 momenta back to the lab frame - rk::Boost boost_from_Drest_to_lab = p4D_lab.labBoost(); - rk::P4 p4K_lab = p4K_Drest.boost(boost_from_Drest_to_lab); - rk::P4 p4W_lab = p4W_Drest.boost(boost_from_Drest_to_lab); - - // this ends the computation of D->W+K/Pi decay, now treat the W->l+nu decay - double ml = particleMass(record.signature.secondary_types[1]); - double mnu = 0; - double W_cosTheta = random->Uniform(-1, 1); // sampling the direction - double W_sinTheta = std::sin(std::acos(W_cosTheta)); - double El = (Q2 + pow(ml, 2)) / (2 * sqrt(Q2)); - double Enu = (Q2 - pow(ml, 2)) / (2 * sqrt(Q2)); // the energies of the outgoing lepton and neutrino - double P = (Q2 - pow(ml, 2)) / (2 * sqrt(Q2)); - // now we have thr four vectors of the outgoing particle kinematics in tne W rest frame wrt x direction - rk::P4 p4l_Wrest(P * geom3::Vector3(W_cosTheta, W_sinTheta, 0), ml); - rk::P4 p4nu_Wrest(P * geom3::Vector3(-W_cosTheta, -W_sinTheta, 0), 0); - - geom3::Vector3 p3W_lab = p4W_lab.momentum(); - geom3::UnitVector3 p3W_lab_dir = p3W_lab.direction(); - geom3::Rotation3 x_to_p3W_lab_rot = geom3::rotationBetween(x_dir, p3W_lab_dir); - p4l_Wrest.rotate(x_to_p3W_lab_rot); - p4nu_Wrest.rotate(x_to_p3W_lab_rot); - // now finally perform the last aximuthal rotation - double W_phi = random->Uniform(0, 2 * M_PI); - geom3::Rotation3 W_azimuth_rand_rot(p3W_lab_dir, W_phi); - p4l_Wrest.rotate(W_azimuth_rand_rot); - p4nu_Wrest.rotate(W_azimuth_rand_rot); - rk::Boost boost_from_Wrest_to_lab = p4W_lab.labBoost(); - rk::P4 p4l_lab = p4l_Wrest.boost(boost_from_Wrest_to_lab); - rk::P4 p4nu_lab = p4nu_Wrest.boost(boost_from_Wrest_to_lab); + // Kinematic limits for m23 (lepton-neutrino invariant mass) + double m23Min = ml + mnu; // minimum: lepton + neutrino at rest + double m23Max = mD - mK; // maximum: kaon at rest + double mDiff = m23Max - m23Min; + + // Maximum phase space weight: p1Max * p23Max + // p1Max = kaon momentum when m23 = m23Min + double p1Max = 0.5 * sqrt((mD - mK - m23Min) * (mD + mK + m23Min) + * (mD + mK - m23Min) * (mD - mK + m23Min)) / mD; + // p23Max = lepton momentum in m23 rest frame when m23 = m23Max + double p23Max = 0.5 * sqrt((m23Max - ml - mnu) * (m23Max + ml + mnu) + * (m23Max + ml - mnu) * (m23Max - ml + mnu)) / m23Max; + double wtPSmax = 0.5 * p1Max * p23Max; + + // V-A matrix element upper bound (from Pythia, meMode == 22) + double wtMEmax = std::min(std::pow(mD, 4) / 16.0, + mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); + double wtME; + + rk::P4 p4K_Drest, p4l_Drest, p4nu_Drest; + + do { + wtME = 1.0; + + // --- Step 1: Sample m23 flat, accept-reject on phase space weight --- + double m23, p1Abs, p23Abs, wtPS; + do { + m23 = m23Min + random->Uniform(0, 1) * mDiff; + + // p1Abs = kaon momentum in D rest frame for this m23 + p1Abs = 0.5 * sqrt((mD - mK - m23) * (mD + mK + m23) + * (mD + mK - m23) * (mD - mK + m23)) / mD; + // p23Abs = lepton momentum in m23 rest frame + p23Abs = 0.5 * sqrt((m23 - ml - mnu) * (m23 + ml + mnu) + * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; + wtPS = p1Abs * p23Abs; + } while (wtPS < random->Uniform(0, 1) * wtPSmax); + + // --- Step 2: Set up m23 -> lepton + neutrino isotropically --- + double cosTheta23 = random->Uniform(-1, 1); + double sinTheta23 = std::sin(std::acos(cosTheta23)); + double phi23 = random->Uniform(0, 2 * M_PI); + + // Lepton and neutrino in m23 rest frame + geom3::Vector3 dir23(sinTheta23 * std::cos(phi23), + sinTheta23 * std::sin(phi23), + cosTheta23); + rk::P4 p4l_m23rest(p23Abs * dir23, ml); + rk::P4 p4nu_m23rest(-p23Abs * dir23, mnu); + + // --- Step 3: Set up D -> K + (m23 system) isotropically --- + double cosTheta1 = random->Uniform(-1, 1); + double sinTheta1 = std::sin(std::acos(cosTheta1)); + double phi1 = random->Uniform(0, 2 * M_PI); + + geom3::Vector3 dir1(sinTheta1 * std::cos(phi1), + sinTheta1 * std::sin(phi1), + cosTheta1); + // Kaon in D rest frame + p4K_Drest = rk::P4(p1Abs * dir1, mK); + // m23 system in D rest frame (opposite to kaon) + rk::P4 p4m23_Drest(-p1Abs * dir1, m23); + + // --- Step 4: Boost lepton and neutrino from m23 rest frame to D rest frame --- + rk::Boost boost_m23_to_Drest = p4m23_Drest.labBoost(); + p4l_Drest = p4l_m23rest.boost(boost_m23_to_Drest); + p4nu_Drest = p4nu_m23rest.boost(boost_m23_to_Drest); + + // --- Step 5: V-A matrix element weight --- + // wtME = m_D * E_lepton * (p_neutrino . p_kaon) in D rest frame + // This is Lorentz invariant up to the m_D * E_l factor which equals p_D . p_l + wtME = mD * p4l_Drest.e() * p4nu_Drest.dot(p4K_Drest); + + } while (wtME < random->Uniform(0, 1) * wtMEmax); + + // --- Boost all particles from D rest frame to lab frame --- + rk::Boost boost_D_to_lab = p4D_lab.labBoost(); + rk::P4 p4K_lab = p4K_Drest.boost(boost_D_to_lab); + rk::P4 p4l_lab = p4l_Drest.boost(boost_D_to_lab); + rk::P4 p4nu_lab = p4nu_Drest.boost(boost_D_to_lab); + + // --- Set secondary particle records --- std::vector & secondaries = record.GetSecondaryParticleRecords(); siren::dataclasses::SecondaryParticleRecord & kpi = secondaries[0]; siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[1]; - siren::dataclasses::SecondaryParticleRecord & neutrino = secondaries[2]; //these are all hardcoded at the time + siren::dataclasses::SecondaryParticleRecord & neutrino = secondaries[2]; kpi.SetFourMomentum({p4K_lab.e(), p4K_lab.px(), p4K_lab.py(), p4K_lab.pz()}); kpi.SetMass(p4K_lab.m()); diff --git a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h index e487ec3bd..9c670ecb6 100644 --- a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h +++ b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h @@ -78,6 +78,7 @@ friend cereal::access; // function definitions needed to compute the DIS vertex double TotalCrossSection(dataclasses::InteractionRecord const &) const override; double TotalCrossSection(siren::dataclasses::ParticleType primary, double energy) const; + double TotalCrossSectionAllFinalStates(dataclasses::InteractionRecord const &) const override; double DifferentialCrossSection(dataclasses::InteractionRecord const &) const override; double DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2=std::numeric_limits::quiet_NaN()) const; double InteractionThreshold(dataclasses::InteractionRecord const &) const override; From 1d430805cb44a52d041d4246dda6158ee368f6f5 Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Wed, 29 Apr 2026 17:03:40 -0400 Subject: [PATCH 32/93] =?UTF-8?q?Sub-merge=201:=20photospline=20patch=20?= =?UTF-8?q?=E2=80=94=20cholmod.h=20outside=20extern=20"C"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modern cholmod.h (suite-sparse 7.x in spack env) transitively includes , which is C++ and forbidden inside extern "C". Move the #include in vendor/photospline/include/photospline/detail/splineutil.h above the extern "C" block. cholmod.h has its own internal extern C wrapping, so the move is ABI-safe. Picked up automatically by cmake/Packages/photospline.cmake glob+--forward logic. --- ...ove-cholmod-include-outside-extern-c.patch | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 cmake/photospline_patches/0002-move-cholmod-include-outside-extern-c.patch diff --git a/cmake/photospline_patches/0002-move-cholmod-include-outside-extern-c.patch b/cmake/photospline_patches/0002-move-cholmod-include-outside-extern-c.patch new file mode 100644 index 000000000..a55eb4afa --- /dev/null +++ b/cmake/photospline_patches/0002-move-cholmod-include-outside-extern-c.patch @@ -0,0 +1,19 @@ +diff --git a/include/photospline/detail/splineutil.h b/include/photospline/detail/splineutil.h +index bd7b985..828cbdc 100644 +--- a/include/photospline/detail/splineutil.h ++++ b/include/photospline/detail/splineutil.h +@@ -2,12 +2,12 @@ + #ifndef PHOTOSPLINE_CFITTER_SPLINEUTIL_H + #define PHOTOSPLINE_CFITTER_SPLINEUTIL_H + ++#include ++ + #ifdef __cplusplus + extern "C" { + #endif + +-#include +- + struct ndsparse { + /* + * This is an ntuple, and is similar to the CHOLMOD triplet From 2cbd4d06996f01d5392bc59491269a409cadd1c7 Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Thu, 30 Apr 2026 17:04:51 -0400 Subject: [PATCH 33/93] QuarkDISFromSpline: reject events with xi >= 1 instead of clamping xi = x(1 + m_c^2/Q^2) is the slow-rescaling shifted Bjorken-x. xi >= 1 is a hard kinematic exclusion (especially at low Q^2 where m_c^2/Q^2 dominates), not a numerical-precision artifact. Clamping to xi = 1 - 1e-5 produced a near-massless spectator and degenerate event kinematics that propagated into the parquet as unphysical events. Throw InjectionFailure so the injector re-rolls, matching the existing rejection pattern at line ~845. Co-Authored-By: Claude Opus 4.7 (1M context) --- projects/interactions/private/QuarkDISFromSpline.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 9cfed5e23..76a62f07f 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -790,7 +790,7 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR double m_c = siren::utilities::Constants::CharmMass; double xi = final_x * (1.0 + m_c * m_c / Q2); if (xi >= 1.0) { - xi = 1.0 - 1e-5; // to avoid unphysical xi values due to numerical precision issues + throw(siren::utilities::InjectionFailure("xi >= 1.0; slow-rescaling pushed Bjorken-x past unity")); } rk::P4 p_parton(geom3::Vector3(0, 0, 0), xi * target_mass_); // parton at rest: (ξM, 0, 0, 0) rk::P4 p4_lab = p_parton + pq_lab; // struck charm = ξ*p2 + q From 99c0d82f2f6fa1b9fd177a8768f8d1460dc04bd8 Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Tue, 5 May 2026 19:26:17 -0400 Subject: [PATCH 34/93] Add c-bar flavor and Ds support to CharmMesonDecay / DMesonELoss Extend primary_types in CharmMesonDecay and DMesonELoss to cover the anti-flavor side (D0Bar, DMinus, DsMinus) and Ds+/Ds-. Sign-conjugate the c-flavor decay-mode sets in TotalDecayWidthForFinalState so c-bar primaries can decay. Ds primaries skip the form-factor CDF cache and rely on the 3-body phase-space sampling done inline in SampleFinalState. Other fixes pulled in here: - ParticleTypes.def: rename DsMinusBar (typo) -> DsMinus. - Particle::isD() recognizes Ds+/Ds-. - particleMass() returns 1.96834 GeV for Ds+/Ds- (PDG 2022). Co-Authored-By: Claude Opus 4.7 (1M context) --- projects/dataclasses/private/Particle.cxx | 3 +- .../SIREN/dataclasses/ParticleTypes.def | 2 +- .../interactions/private/CharmMesonDecay.cxx | 250 +++++++++++++++--- .../SIREN/interactions/CharmMesonDecay.h | 2 +- .../public/SIREN/interactions/DMesonELoss.h | 2 +- 5 files changed, 213 insertions(+), 46 deletions(-) diff --git a/projects/dataclasses/private/Particle.cxx b/projects/dataclasses/private/Particle.cxx index eb366fb73..5a55fe480 100644 --- a/projects/dataclasses/private/Particle.cxx +++ b/projects/dataclasses/private/Particle.cxx @@ -108,7 +108,8 @@ bool isHadron(Particle::ParticleType p){ bool isD(Particle::ParticleType p){ return(p==Particle::ParticleType::D0 || p==Particle::ParticleType::D0Bar || - p==Particle::ParticleType::DPlus || p==Particle::ParticleType::DMinus); + p==Particle::ParticleType::DPlus || p==Particle::ParticleType::DMinus || + p==Particle::ParticleType::DsPlus || p==Particle::ParticleType::DsMinus); } } // namespace utilities diff --git a/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def b/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def index af36148c8..f9d6a251e 100644 --- a/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def +++ b/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def @@ -43,7 +43,7 @@ X(DMinus, -411) X(D0, 421) X(D0Bar, -421) X(DsPlus, 431) -X(DsMinusBar, -431) +X(DsMinus, -431) X(LambdacPlus, 4122) X(WPlus, 24) X(WMinus, -24) diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx index 24298d8dc..f4b0e0ac4 100644 --- a/projects/interactions/private/CharmMesonDecay.cxx +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -47,7 +47,11 @@ CharmMesonDecay::CharmMesonDecay(siren::dataclasses::Particle::ParticleType prim double mD; double mK; - if (primary == siren::dataclasses::Particle::ParticleType::DPlus) { + // Form-factor constants are CP-symmetric (|V_cs|, alpha, m_D*); the cached + // inverseCdf table is dead code in current SIREN, so anti-flavor instances + // share the same body as their particle counterparts. + if (primary == siren::dataclasses::Particle::ParticleType::DPlus || + primary == siren::dataclasses::Particle::ParticleType::DMinus) { constants[0] = 0.725; // this is f^+(0)|V_cs| for charged D constants[1] = 0.44; // this is alpha, same for all K final states constants[2] = 2.01027; // this is excited charged D meson @@ -55,13 +59,21 @@ CharmMesonDecay::CharmMesonDecay(siren::dataclasses::Particle::ParticleType prim mD = particleMass(siren::dataclasses::Particle::ParticleType::DPlus); mK = particleMass(siren::dataclasses::Particle::ParticleType::K0Bar); - } else if (primary == siren::dataclasses::Particle::ParticleType::D0) { + } else if (primary == siren::dataclasses::Particle::ParticleType::D0 || + primary == siren::dataclasses::Particle::ParticleType::D0Bar) { constants[0] = 0.719; // this is f^+(0)|V_cs| for charged D constants[1] = 0.50; // this is alpha, same for all K final states constants[2] = 2.00697; // this is excited charged D meson mD = particleMass(siren::dataclasses::Particle::ParticleType::D0); mK = particleMass(siren::dataclasses::Particle::ParticleType::KMinus); + } else if (primary == siren::dataclasses::Particle::ParticleType::DsPlus || + primary == siren::dataclasses::Particle::ParticleType::DsMinus) { + // Ds -> (eta / eta' / phi) + mu + nu uses pure 3-body phase space (no + // form factor). Daughter is sampled inline in SampleFinalState. The + // computeDiffGammaCDF table is only consumed by D+/D0 form-factor logic, + // so skip it here. + return; } computeDiffGammaCDF(constants, mD, mK); @@ -108,6 +120,10 @@ double CharmMesonDecay::particleMass(siren::dataclasses::ParticleType particle) return( siren::utilities::Constants::tauMass ); case siren::dataclasses::ParticleType::TauMinus: return( siren::utilities::Constants::tauMass ); + case siren::dataclasses::ParticleType::DsPlus: + return 1.96834; // GeV (PDG 2022); not in Constants.h + case siren::dataclasses::ParticleType::DsMinus: + return 1.96834; default: return(0.0); } @@ -153,20 +169,76 @@ double CharmMesonDecay::TotalDecayWidthForFinalState(dataclasses::InteractionRec std::set kminus_muplus_numu = {siren::dataclasses::Particle::ParticleType::KMinus, siren::dataclasses::Particle::ParticleType::MuPlus, siren::dataclasses::Particle::ParticleType::NuMu}; + std::set hadrons_muplus_numu = {siren::dataclasses::Particle::ParticleType::Hadrons, + siren::dataclasses::Particle::ParticleType::MuPlus, + siren::dataclasses::Particle::ParticleType::NuMu}; std::set hadrons = {siren::dataclasses::Particle::ParticleType::Hadrons}; + // Anti-flavor (c̄) decay-mode sets, sign-conjugated from the c modes above. + std::set k0_eminus_nuebar = {siren::dataclasses::Particle::ParticleType::K0, + siren::dataclasses::Particle::ParticleType::EMinus, + siren::dataclasses::Particle::ParticleType::NuEBar}; + std::set kplus_eminus_nuebar = {siren::dataclasses::Particle::ParticleType::KPlus, + siren::dataclasses::Particle::ParticleType::EMinus, + siren::dataclasses::Particle::ParticleType::NuEBar}; + std::set k0_muminus_numubar = {siren::dataclasses::Particle::ParticleType::K0, + siren::dataclasses::Particle::ParticleType::MuMinus, + siren::dataclasses::Particle::ParticleType::NuMuBar}; + std::set kplus_muminus_numubar = {siren::dataclasses::Particle::ParticleType::KPlus, + siren::dataclasses::Particle::ParticleType::MuMinus, + siren::dataclasses::Particle::ParticleType::NuMuBar}; + std::set hadrons_muminus_numubar = {siren::dataclasses::Particle::ParticleType::Hadrons, + siren::dataclasses::Particle::ParticleType::MuMinus, + siren::dataclasses::Particle::ParticleType::NuMuBar}; if (primary == siren::dataclasses::Particle::ParticleType::DPlus) { tau = 1040 * (1e-15); - // if (secondaries == k0_eplus_nue) {branching_ratio = .1607;} // e+ semileptonic mode according to pdg - // else if (secondaries == k0_muplus_numu) {branching_ratio = .176;} // mu+ anything according to pdg - // else if (secondaries == hadrons) {branching_ratio = (1 - .1607 - .176);} // everything else - if (secondaries == k0_eplus_nue) {branching_ratio = .1607;} // e+ semileptonic mode according to pdg - else if (secondaries == k0_muplus_numu) {branching_ratio = .176;} // mu+ anything according to pdg (K + K* combined) - else if (secondaries == hadrons) {branching_ratio = (1 - .1607 - .176);} // everything else + // MODIFIED 2025-04-09: Force all decays to muonic semileptonic channel + // to maximize dimuon statistics. Must multiply event weights by physical BR + // in post-processing (D+ -> mu: 0.176, D+ -> e: 0.1607, D+ -> hadrons: 0.6633). + // To restore physical BRs, uncomment the original lines below: + // if (secondaries == k0_eplus_nue) {branching_ratio = .1607;} + // else if (secondaries == k0_muplus_numu) {branching_ratio = .176;} + // else if (secondaries == hadrons) {branching_ratio = (1 - .1607 - .176);} + if (secondaries == k0_eplus_nue) {branching_ratio = 0.0;} + else if (secondaries == k0_muplus_numu) {branching_ratio = 1.0;} + else if (secondaries == hadrons) {branching_ratio = 0.0;} + } else if (primary == siren::dataclasses::Particle::ParticleType::DMinus) { + // CP-mirror of D+ (same lifetime, same forced muonic BR convention). + tau = 1040 * (1e-15); + if (secondaries == k0_eminus_nuebar) {branching_ratio = 0.0;} + else if (secondaries == k0_muminus_numubar) {branching_ratio = 1.0;} + else if (secondaries == hadrons) {branching_ratio = 0.0;} } else if (primary == siren::dataclasses::Particle::ParticleType::D0) { tau = 410.1 * (1e-15); - if (secondaries == kminus_eplus_nue) {branching_ratio = .0649;} // e+ semileptonic mode according to pdg - else if (secondaries == kminus_muplus_numu) {branching_ratio = .067;} // mu+ anything according to pdg (K + K* combined) - else if (secondaries == hadrons) {branching_ratio = (1 - .0649 - .067);} // everything else + // MODIFIED 2025-04-09: Force all decays to muonic semileptonic channel + // to maximize dimuon statistics. Must multiply event weights by physical BR + // in post-processing (D0 -> mu: 0.067, D0 -> e: 0.0649, D0 -> hadrons: 0.8681). + // To restore physical BRs, uncomment the original lines below: + // if (secondaries == kminus_eplus_nue) {branching_ratio = .0649;} + // else if (secondaries == kminus_muplus_numu) {branching_ratio = .067;} + // else if (secondaries == hadrons) {branching_ratio = (1 - .0649 - .067);} + if (secondaries == kminus_eplus_nue) {branching_ratio = 0.0;} + else if (secondaries == kminus_muplus_numu) {branching_ratio = 1.0;} + else if (secondaries == hadrons) {branching_ratio = 0.0;} + } else if (primary == siren::dataclasses::Particle::ParticleType::D0Bar) { + // CP-mirror of D0. + tau = 410.1 * (1e-15); + if (secondaries == kplus_eminus_nuebar) {branching_ratio = 0.0;} + else if (secondaries == kplus_muminus_numubar) {branching_ratio = 1.0;} + else if (secondaries == hadrons) {branching_ratio = 0.0;} + } else if (primary == siren::dataclasses::Particle::ParticleType::DsPlus) { + tau = 504 * (1e-15); // Ds+ lifetime: 504 fs (PDG) + // Force BR=1.0 for the muonic semileptonic channel (Ds -> Hadrons mu nu), + // matching the D+/D0 convention. Multiply by physical inclusive BR + // (Ds -> mu X ~ 0.0654, no tau feed-down) in post-processing. + // Daughter "Hadrons" stands in for eta / eta' / phi (sampled in + // SampleFinalState with fractions 0.46 / 0.16 / 0.38). + if (secondaries == hadrons_muplus_numu) {branching_ratio = 1.0;} + else if (secondaries == hadrons) {branching_ratio = 0.0;} + } else if (primary == siren::dataclasses::Particle::ParticleType::DsMinus) { + // CP-mirror of Ds+ (eta/eta'/phi are self-conjugate, only lepton/nu flip). + tau = 504 * (1e-15); + if (secondaries == hadrons_muminus_numubar) {branching_ratio = 1.0;} + else if (secondaries == hadrons) {branching_ratio = 0.0;} } else { std::cout << "this decay mode is not yet implemented!" << std::endl; @@ -210,11 +282,23 @@ std::vector CharmMesonDecay::GetPossibleSigna // all other modes implemented as one big bang hadron_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; signatures.push_back(hadron_signature); + } else if (primary==siren::dataclasses::Particle::ParticleType::DMinus) { + // CP-mirror of D+: K0 (s̄d → contains s̄), e⁻/μ⁻, ν̄_e/ν̄_μ. + semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::K0; + semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::EMinus; + semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuEBar; + signatures.push_back(semilep_signature); + semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::K0; + semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::MuMinus; + semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuMuBar; + signatures.push_back(semilep_signature); + hadron_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; + signatures.push_back(hadron_signature); } else if (primary==siren::dataclasses::Particle::ParticleType::D0) { semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::KMinus; semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::EPlus; semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuE; - + signatures.push_back(semilep_signature); semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::KMinus; semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::MuPlus; @@ -222,6 +306,38 @@ std::vector CharmMesonDecay::GetPossibleSigna signatures.push_back(semilep_signature); hadron_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; signatures.push_back(hadron_signature); + } else if (primary==siren::dataclasses::Particle::ParticleType::D0Bar) { + // CP-mirror of D0: K+ (us̄), e⁻/μ⁻, ν̄_e/ν̄_μ. + semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::KPlus; + semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::EMinus; + semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuEBar; + signatures.push_back(semilep_signature); + semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::KPlus; + semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::MuMinus; + semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuMuBar; + signatures.push_back(semilep_signature); + hadron_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; + signatures.push_back(hadron_signature); + } else if (primary==siren::dataclasses::Particle::ParticleType::DsPlus) { + // Ds: only the muonic semileptonic channel (Ds -> Hadrons mu nu) plus + // the all-hadronic catch-all. No e+ ve channel — would just sit at BR=0 + // anyway under the current "force-muonic" convention. + // The Hadrons daughter stands in for eta / eta' / phi (sampled inline + // in SampleFinalState; SIREN ParticleType has no entries for these). + semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; + semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::MuPlus; + semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuMu; + signatures.push_back(semilep_signature); + hadron_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; + signatures.push_back(hadron_signature); + } else if (primary==siren::dataclasses::Particle::ParticleType::DsMinus) { + // CP-mirror of Ds+: eta/eta'/phi are self-conjugate, only lepton/nu flip. + semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; + semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::MuMinus; + semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuMuBar; + signatures.push_back(semilep_signature); + hadron_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; + signatures.push_back(hadron_signature); } else { std::cout << "this D meson decay has not been implemented yet" << std::endl; @@ -234,11 +350,14 @@ std::vector CharmMesonDecay::FormFactorFromRecord(dataclasses::CrossSect std::vector constants; constants.resize(3); // check the primary and secondaries of the signature - if (signature.primary_type == dataclasses::Particle::ParticleType::DPlus && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::K0Bar) { + // Form-factor constants are CP-symmetric — anti-flavor cases mirror the c cases. + if ((signature.primary_type == dataclasses::Particle::ParticleType::DPlus && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::K0Bar) || + (signature.primary_type == dataclasses::Particle::ParticleType::DMinus && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::K0)) { constants[0] = 0.725; // this is f^+(0)|V_cs| for charged D constants[1] = 0.44; // this is alpha, same for all K final states constants[2] = 2.01027; // this is excited charged D meson - } else if (signature.primary_type == dataclasses::Particle::ParticleType::D0 && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::KMinus) { + } else if ((signature.primary_type == dataclasses::Particle::ParticleType::D0 && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::KMinus) || + (signature.primary_type == dataclasses::Particle::ParticleType::D0Bar && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::KPlus)) { constants[0] = 0.719; // this is f^+(0)|V_cs| for neutral D constants[1] = 0.50; // this is alpha, same for all K final states constants[2] = 2.00697; // this is excited neutral D meson @@ -247,9 +366,20 @@ std::vector CharmMesonDecay::FormFactorFromRecord(dataclasses::CrossSect } double CharmMesonDecay::DifferentialDecayWidth(dataclasses::InteractionRecord const & record) const { - // first let the fully hadronic state be handled separately + // first let the fully hadronic state be handled separately. Use size==1 so + // we don't mis-route the Ds semileptonic mode (Hadrons + mu + nu, size 3). dataclasses::InteractionSignature signature = record.signature; - if (signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::Hadrons) { + if (signature.secondary_types.size() == 1 && + signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::Hadrons) { + return TotalDecayWidthForFinalState(record); + } + // Ds semileptonic uses pure phase space (no form factor); FinalStateProbability + // for Ds is dd/td which is handled by the matrix-element-free flat sampling. + // Returning the total width here makes FinalStateProbability = 1, which is the + // right thing when the kinematic distribution is sampled directly from phase + // space (no reweighting needed). + if (signature.primary_type == siren::dataclasses::Particle::ParticleType::DsPlus || + signature.primary_type == siren::dataclasses::Particle::ParticleType::DsMinus) { return TotalDecayWidthForFinalState(record); } // get the form factor constants @@ -357,9 +487,11 @@ void CharmMesonDecay::SampleFinalStateHadronic(dataclasses::CrossSectionDistribu } void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { - // first handle hadronic decay separately + // first handle hadronic decay separately. Use size==1 so we don't + // mis-route the Ds semileptonic mode (Hadrons + mu + nu, size 3). dataclasses::InteractionSignature signature = record.signature; - if (signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::Hadrons) { + if (signature.secondary_types.size() == 1 && + signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::Hadrons) { SampleFinalStateHadronic(record, random); return; } @@ -368,32 +500,56 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco // 3-body phase space sampling following Pythia's approach // (ParticleDecays::threeBody in ParticleDecays.cc) // - // D (m0) -> K (m1) + lepton (m2) + neutrino (m3) + // D (m0) -> hadron (m1) + lepton (m2) + neutrino (m3) // // Phase space: sample m23 (lepton-neutrino invariant mass = sqrt(q^2)) // flat in allowed range, accept-reject on phase space weight. - // Then apply V-A matrix element correction. + // For D+/D0: apply V-A matrix element correction (K vs K* with fixed ratio). + // For Ds: pure 3-body phase space, no V-A correction. Daughter is + // sampled inline as eta / eta' / phi with fractions 0.46 / 0.16 + // / 0.38 (from Ds->eta/eta'/phi mu nu BRs of 2.3/0.8/1.9 %). // ========================================================================= - double mD = particleMass(record.signature.primary_type); // m0 - double mK_base = particleMass(record.signature.secondary_types[0]); // m1 (K mass from signature) - double ml = particleMass(record.signature.secondary_types[1]); // m2 - double mnu = 0.0; // m3 - - // Randomly choose between D->K l nu and D->K* l nu channels - // based on their relative branching ratios within the muonic mode. - // Pythia uses the same V-A matrix element (meMode=22) for both; - // only the hadron mass differs: K(494 MeV) vs K*(892 MeV). - double mKstar = 0.89166; // K*(892) mass in GeV - double fracK; // fraction of events that are D->K (vs D->K*) - if (record.signature.primary_type == siren::dataclasses::Particle::ParticleType::DPlus) { - // D+ -> K0bar mu+ nu: BR=8.74%, D+ -> K*0bar mu+ nu: BR=5.33% - fracK = 8.74 / (8.74 + 5.33); // ~0.621 + siren::dataclasses::Particle::ParticleType primary = record.signature.primary_type; + bool is_Ds = (primary == siren::dataclasses::Particle::ParticleType::DsPlus || + primary == siren::dataclasses::Particle::ParticleType::DsMinus); + + double mD = particleMass(primary); // m0 + double ml = particleMass(record.signature.secondary_types[1]); // m2 + double mnu = 0.0; // m3 + + double mK; // m1: hadron daughter mass (chosen per-decay) + if (is_Ds) { + // Ds -> eta mu nu (BR 2.3%), Ds -> eta' mu nu (0.8%), Ds -> phi mu nu (1.9%) + // Cumulative: eta=0.46, eta+eta'=0.62, all=1.0 + double mEta = siren::utilities::Constants::EtaMass; // 0.547862 GeV + double mEtaPrime = siren::utilities::Constants::EtaPrimeMass; // 0.95778 GeV + double mPhi = 1.01946; // GeV (PDG); not in Constants.h + double r = random->Uniform(0, 1); + if (r < 2.3 / 5.0) { + mK = mEta; + } else if (r < 3.1 / 5.0) { + mK = mEtaPrime; + } else { + mK = mPhi; + } } else { - // D0 -> K- mu+ nu: BR=3.41%, D0 -> K*- mu+ nu: BR=2.17% - fracK = 3.41 / (3.41 + 2.17); // ~0.611 + // D+/D0: K vs K*(892) with V-A weighting + double mK_base = particleMass(record.signature.secondary_types[0]); + double mKstar = 0.89166; // K*(892) mass in GeV + double fracK; + if (primary == siren::dataclasses::Particle::ParticleType::DPlus || + primary == siren::dataclasses::Particle::ParticleType::DMinus) { + // D+ -> K0bar mu+ nu: BR=8.74%, D+ -> K*0bar mu+ nu: BR=5.33% + // (D- BRs identical by CP.) + fracK = 8.74 / (8.74 + 5.33); // ~0.621 + } else { + // D0 -> K- mu+ nu: BR=3.41%, D0 -> K*- mu+ nu: BR=2.17% + // (D0bar BRs identical by CP.) + fracK = 3.41 / (3.41 + 2.17); // ~0.611 + } + mK = (random->Uniform(0, 1) < fracK) ? mK_base : mKstar; } - double mK = (random->Uniform(0, 1) < fracK) ? mK_base : mKstar; // D meson 4-momentum in lab frame rk::P4 p4D_lab(geom3::Vector3(record.primary_momentum[1], @@ -416,9 +572,14 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco * (m23Max + ml - mnu) * (m23Max - ml + mnu)) / m23Max; double wtPSmax = 0.5 * p1Max * p23Max; - // V-A matrix element upper bound (from Pythia, meMode == 22) - double wtMEmax = std::min(std::pow(mD, 4) / 16.0, - mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); + // V-A matrix element upper bound (from Pythia, meMode == 22). + // For Ds we skip V-A weighting and use pure 3-body phase space — set + // wtMEmax = 1.0 and wtME = 1.0 inside the loop so the rejection test + // (wtME < rand * wtMEmax) is always false and we exit after one iteration. + double wtMEmax = is_Ds + ? 1.0 + : std::min(std::pow(mD, 4) / 16.0, + mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); double wtME; rk::P4 p4K_Drest, p4l_Drest, p4nu_Drest; @@ -470,10 +631,15 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco p4l_Drest = p4l_m23rest.boost(boost_m23_to_Drest); p4nu_Drest = p4nu_m23rest.boost(boost_m23_to_Drest); - // --- Step 5: V-A matrix element weight --- + // --- Step 5: V-A matrix element weight (skipped for Ds — pure phase space) --- // wtME = m_D * E_lepton * (p_neutrino . p_kaon) in D rest frame // This is Lorentz invariant up to the m_D * E_l factor which equals p_D . p_l - wtME = mD * p4l_Drest.e() * p4nu_Drest.dot(p4K_Drest); + if (!is_Ds) { + wtME = mD * p4l_Drest.e() * p4nu_Drest.dot(p4K_Drest); + } + // For Ds: wtME stays at 1.0 (set at the top of the do-loop). Combined with + // wtMEmax = 1.0, the test (wtME < rand[0,1) * wtMEmax) is always false, + // so we exit after one iteration with no V-A reweighting. } while (wtME < random->Uniform(0, 1) * wtMEmax); diff --git a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h index c5bf3e84f..b1b3d6345 100644 --- a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h +++ b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h @@ -34,7 +34,7 @@ namespace interactions { class CharmMesonDecay : public Decay { friend cereal::access; private: - const std::set primary_types = {siren::dataclasses::Particle::ParticleType::D0, siren::dataclasses::Particle::ParticleType::DPlus}; + const std::set primary_types = {siren::dataclasses::Particle::ParticleType::D0, siren::dataclasses::Particle::ParticleType::DPlus, siren::dataclasses::Particle::ParticleType::DsPlus, siren::dataclasses::Particle::ParticleType::D0Bar, siren::dataclasses::Particle::ParticleType::DMinus, siren::dataclasses::Particle::ParticleType::DsMinus}; siren::utilities::Interpolator1D inverseCdf; // for dGamma public: CharmMesonDecay(); diff --git a/projects/interactions/public/SIREN/interactions/DMesonELoss.h b/projects/interactions/public/SIREN/interactions/DMesonELoss.h index d6d5097d3..4244b5ec0 100644 --- a/projects/interactions/public/SIREN/interactions/DMesonELoss.h +++ b/projects/interactions/public/SIREN/interactions/DMesonELoss.h @@ -37,7 +37,7 @@ namespace interactions { class DMesonELoss : public CrossSection { friend cereal::access; private: - std::set primary_types_ = {siren::dataclasses::Particle::ParticleType::D0, siren::dataclasses::Particle::ParticleType::DPlus}; + std::set primary_types_ = {siren::dataclasses::Particle::ParticleType::D0, siren::dataclasses::Particle::ParticleType::DPlus, siren::dataclasses::Particle::ParticleType::DsPlus, siren::dataclasses::Particle::ParticleType::D0Bar, siren::dataclasses::Particle::ParticleType::DMinus, siren::dataclasses::Particle::ParticleType::DsMinus}; std::set target_types_ = {siren::dataclasses::Particle::ParticleType::PPlus}; public: From 524178560a793a490476b58637db6e61461ab81e Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Tue, 5 May 2026 19:27:39 -0400 Subject: [PATCH 35/93] PythiaDISCrossSection: Ds + isoscalar target + per-event reinit Summary of changes that accumulated in this file during the merge: * Ds (431) added to IsCharmedHadron and to D_types_; charmed-meson identification in getIndices switched to "by elimination" so it works for any charm hadron, not just the D0/D+/- that isD() recognizes. * SampleFinalState const_cast-overwrites the signature meson slot with Pythia\s actual produced PID. This is the mechanism by which c-bar events get the correct D0Bar/D-/Ds- secondary type. * Pythia is reinitialized every event so Beams:eA tracks E_nu. Pythia 8\s variable-energy mode is not supported for WeakBosonExchange, so setKinematics cannot be used; ~1 s/event is the cost of correctness. * Target nucleon is now sampled per event from H2O isoscalar composition (10/18 proton, 8/18 neutron) via the SIREN RNG, instead of always 2212. InitializePythia gains a target_pdg argument; target_pdg is recorded in interaction_parameters for downstream analyses. * FinalStateProbability now returns dsigma/sigma (was 1.0). Removing the PDG-average FragmentationFraction multiplication avoids double-counting with Pythia\s natural Lund-string fragmentation; the resulting weighting matches DISFromSpline\s convention. The per-flavor FragmentationFraction table is kept for API compatibility but is no longer consumed in the weighting path. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../private/PythiaDISCrossSection.cxx | 76 ++++++++++++------- .../interactions/PythiaDISCrossSection.h | 2 +- 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/projects/interactions/private/PythiaDISCrossSection.cxx b/projects/interactions/private/PythiaDISCrossSection.cxx index 26d6e98e1..b21f82b5d 100644 --- a/projects/interactions/private/PythiaDISCrossSection.cxx +++ b/projects/interactions/private/PythiaDISCrossSection.cxx @@ -157,11 +157,8 @@ bool PythiaDISCrossSection::equal(CrossSection const & other) const { bool PythiaDISCrossSection::IsCharmedHadron(int pdgId) { int abs_id = std::abs(pdgId); - // Only match D0 (421) and D+/- (411) for now. - // TODO: Add Ds (431) and Lambda_c (4122) support — need CharmMesonDecay - // to handle these types before they can be registered as signatures. - // Currently Ds/Lambda_c end up in the hadronic remnant. - return (abs_id == 411 || abs_id == 421); + // Match D0 (421), D+/- (411), Ds (431). Lambda_c (4122) still routed to hadronic remnant. + return (abs_id == 411 || abs_id == 421 || abs_id == 431); } siren::dataclasses::ParticleType PythiaDISCrossSection::PdgToParticleType(int pdgId) { @@ -194,16 +191,21 @@ double PythiaDISCrossSection::GetHadronMass(siren::dataclasses::ParticleType had } std::map PythiaDISCrossSection::getIndices(siren::dataclasses::InteractionSignature signature) { + // Identify meson by elimination (not via isD(), which only covers D0/D±). + // First pass: claim lepton and Hadrons. Second pass: whatever remains is the meson. int lepton_id = -1, hadron_id = -1, meson_id = -1; for (size_t i = 0; i < signature.secondary_types.size(); i++) { if (siren::dataclasses::isLepton(signature.secondary_types[i])) { lepton_id = i; - } else if (siren::dataclasses::isD(signature.secondary_types[i])) { - meson_id = i; - } else { + } else if (signature.secondary_types[i] == siren::dataclasses::ParticleType::Hadrons) { hadron_id = i; } } + for (size_t i = 0; i < signature.secondary_types.size(); i++) { + if ((int)i == lepton_id || (int)i == hadron_id) continue; + meson_id = i; + break; + } return {{"lepton", lepton_id}, {"hadron", hadron_id}, {"meson", meson_id}}; } @@ -248,12 +250,11 @@ void PythiaDISCrossSection::InitializeSignatures() { // Hadron remnant signature.secondary_types.push_back(siren::dataclasses::ParticleType::Hadrons); - // Charmed meson types: always D0 and DPlus regardless of nu/nubar. - // This matches the existing CharmHadronization convention and ensures - // compatibility with CharmMesonDecay (which only supports D0/DPlus). - // TODO: Add Ds (431) and Lambda_c (4122) support. + // Charmed meson types: D0, D+, Ds (Ds support added to match SIREN_outputs 0420 run). + // TODO: Add Lambda_c (4122) support. D_types_ = {siren::dataclasses::ParticleType::D0, - siren::dataclasses::ParticleType::DPlus}; + siren::dataclasses::ParticleType::DPlus, + siren::dataclasses::ParticleType::DsPlus}; for (auto meson_type : D_types_) { dataclasses::InteractionSignature full_signature = signature; @@ -357,16 +358,25 @@ double PythiaDISCrossSection::FragmentationFraction(siren::dataclasses::Particle return 0.6; } else if (secondary == siren::dataclasses::ParticleType::DPlus || secondary == siren::dataclasses::ParticleType::DMinus) { return 0.23; + } else if (secondary == siren::dataclasses::ParticleType::DsPlus || secondary == siren::dataclasses::ParticleType::DsMinus) { + return 0.08; } - // TODO: Add Ds (~0.08) and Lambda_c (~0.09) when signatures include them + // TODO: Add Lambda_c (~0.09) when signatures include them return 0; } double PythiaDISCrossSection::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { - // Pythia samples final-state kinematics from the physical distribution, - // so no differential reweighting is needed. FinalStateProbability appears - // in both physical_probability and generation_probability and cancels. - return 1.0; + // Trust Pythia: SampleFinalState accepts whatever charm meson Pythia + // produces and overwrites the signature's meson_type to match. The natural + // Lund-string fragmentation distribution IS the physical fragfrac, so we + // don't multiply by a PDG-average fragfrac table here — that would double- + // count. Return dσ/σ only (matches DISFromSpline). + double dxs = DifferentialCrossSection(interaction); + double txs = TotalCrossSection(interaction); + if (!std::isfinite(dxs) || !std::isfinite(txs) || dxs <= 0 || txs <= 0) return 0.0; + double result = dxs / txs; + if (!std::isfinite(result)) return 0.0; + return result; } // ── Signature accessors ── @@ -403,7 +413,7 @@ std::vector PythiaDISCrossSection::DensityVariables() const { // Pythia initialization and SampleFinalState — the core new logic // ══════════════════════════════════════════════════════════════════════ -void PythiaDISCrossSection::InitializePythia(double E_nu) const { +void PythiaDISCrossSection::InitializePythia(double E_nu, int target_pdg) const { // Ensure LHAPDF can find PDF sets — derive data path from the LHAPDF library location // The pdf_set_ is e.g. "LHAPDF6:HERAPDF20_NLO_EIG", and LHAPDF needs LHAPDF_DATA_PATH set const char* lhapdf_path = std::getenv("LHAPDF_DATA_PATH"); @@ -432,7 +442,7 @@ void PythiaDISCrossSection::InitializePythia(double E_nu) const { // Beam setup: fixed target pythia_->readString("Beams:frameType = 2"); pythia_->readString("Beams:idA = " + std::to_string(beam_id)); - pythia_->readString("Beams:idB = 2212"); + pythia_->readString("Beams:idB = " + std::to_string(target_pdg)); pythia_->readString("Beams:eA = " + std::to_string(E_nu)); pythia_->readString("Beams:eB = 0."); @@ -490,13 +500,18 @@ void PythiaDISCrossSection::SampleFinalState(dataclasses::CrossSectionDistributi double E_nu = p1.e(); geom3::UnitVector3 nu_dir = p1.momentum().direction(); - // Initialize or update Pythia - if (!pythia_initialized_) { - InitializePythia(E_nu); - } + // Sample target nucleon PDG: 10/18 proton (2212), 8/18 neutron (2112) + // to match H2O composition of ice (10 protons, 8 neutrons per molecule). + // Pythia requires a concrete hadron beam; SIREN's "Nucleon" abstraction + // is resolved here, per event, via the SIREN RNG. + int target_pdg = (random->Uniform(0.0, 1.0) < (10.0 / 18.0)) ? 2212 : 2112; - // For now, reinitialize if energy changes significantly - // TODO: test setKinematics stability across energy ranges + // Re-initialize Pythia every event so Beams:eA tracks this event's E_nu. + // Pythia 8's variable-energy mode is not supported for WeakBosonExchange + // processes, so setKinematics cannot be used here. Rebuilding the Pythia + // object is expensive (~1 s/event) but is the only way to keep the + // sampled muon kinematics consistent with the event's true beam energy. + InitializePythia(E_nu, target_pdg); // Bridge the SIREN RNG for this event siren_rndm_->rng_ = random; @@ -533,6 +548,14 @@ void PythiaDISCrossSection::SampleFinalState(dataclasses::CrossSectionDistributi } if (i_muon >= 0 && i_charm >= 0) { + // Trust Pythia: accept whatever charm meson it produced and + // overwrite the signature's pre-chosen (uniform) meson slot with + // Pythia's actual PID. Natural Lund-string fragmentation then + // carries the physical D-type distribution without rejection. + int pythia_pid = pythia_->event[i_charm].id(); + const_cast(record.signature) + .secondary_types[meson_index] = PdgToParticleType(pythia_pid); + found_charm = true; // Extract 4-momenta from Pythia (in Pythia's frame: nu along +z) @@ -548,6 +571,7 @@ void PythiaDISCrossSection::SampleFinalState(dataclasses::CrossSectionDistributi record.interaction_parameters["energy"] = E_nu; record.interaction_parameters["bjorken_x"] = pythia_x; record.interaction_parameters["bjorken_y"] = pythia_y; + record.interaction_parameters["target_pdg"] = static_cast(target_pdg); // Rotate from Pythia frame (+z) to SIREN's neutrino direction geom3::UnitVector3 z_dir = geom3::UnitVector3::zAxis(); diff --git a/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h b/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h index 4b967ea14..f3450ab0d 100644 --- a/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h +++ b/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h @@ -72,7 +72,7 @@ friend cereal::access; std::string pythia_data_path_; // Helper methods - void InitializePythia(double E_nu) const; + void InitializePythia(double E_nu, int target_pdg) const; void InitializeSignatures(); void LoadFromFile(std::string differential_filename, std::string total_filename); void LoadFromMemory(std::vector & differential_data, std::vector & total_data); From e97f3408eca72bd247da787ed36eecaf04831ed6 Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Tue, 5 May 2026 19:31:37 -0400 Subject: [PATCH 36/93] PythiaDISCrossSection: align Ds fragfrac with thesis convention (0.15) The Ds entry in the legacy FragmentationFraction table was 0.08 (the PDG inclusive value); change it to 0.15 to match the 0.60:0.23:0.15 convention used elsewhere in SIREN (CharmHadronization, QuarkDISFromSpline). The table is no longer consumed in weighting since FinalStateProbability returns dsigma/sigma directly, but keeping the constants consistent across hadronization paths avoids a future foot-gun if the table is ever re-enabled. Co-Authored-By: Claude Opus 4.7 (1M context) --- projects/interactions/private/PythiaDISCrossSection.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/interactions/private/PythiaDISCrossSection.cxx b/projects/interactions/private/PythiaDISCrossSection.cxx index b21f82b5d..c41285653 100644 --- a/projects/interactions/private/PythiaDISCrossSection.cxx +++ b/projects/interactions/private/PythiaDISCrossSection.cxx @@ -359,7 +359,7 @@ double PythiaDISCrossSection::FragmentationFraction(siren::dataclasses::Particle } else if (secondary == siren::dataclasses::ParticleType::DPlus || secondary == siren::dataclasses::ParticleType::DMinus) { return 0.23; } else if (secondary == siren::dataclasses::ParticleType::DsPlus || secondary == siren::dataclasses::ParticleType::DsMinus) { - return 0.08; + return 0.15; } // TODO: Add Lambda_c (~0.09) when signatures include them return 0; From 8560b142b083f4dbf02041d276cd567bb4459f7c Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Fri, 8 May 2026 14:45:17 -0400 Subject: [PATCH 37/93] Switch QuarkDISFromSpline to (xi, y) slow-rescaling sampling Replaces (x_BJ, y) sampling with direct (xi, y) sampling against Maboi dsdxidy splines. Adds slow-rescaling helpers, a leaner kinematic check (Q^2 > 0 charm threshold and W^2 > (M_N+M_D0)^2), and a tight bounding-rectangle for the Metropolis-Hastings proposal. Stores bjorken_xi alongside the recovered bjorken_x in the interaction record so downstream consumers keep the standard meaning. Spec: docs/superpowers/specs/2026-05-08-quarkdis-slow-rescaling-design.md --- .../private/QuarkDISFromSpline.cxx | 201 ++++++++++-------- .../SIREN/interactions/QuarkDISFromSpline.h | 13 +- 2 files changed, 127 insertions(+), 87 deletions(-) diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 76a62f07f..887073597 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -34,35 +34,42 @@ namespace siren { namespace interactions { namespace { -///Check whether a given point in phase space is physically realizable. -///Based on equations 6-8 of http://dx.doi.org/10.1103/PhysRevD.66.113007 -///S. Kretzer and M. H. Reno -///"Tau neutrino deep inelastic charged current interactions" -///Phys. Rev. D 66, 113007 -///\param x Bjorken x of the interaction -///\param y Bjorken y of the interaction -///\param E Incoming neutrino in energy in the lab frame ($E_\nu$) -///\param M Mass of the target nucleon ($M_N$) -///\param m Mass of the secondary lepton ($m_\tau$) -bool kinematicallyAllowed(double x, double y, double E, double M, double m) { - if(x > 1) //Eq. 6 right inequality - return false; - if(x < ((m * m) / (2 * M * (E - m)))) //Eq. 6 left inequality - return false; - if (x < 1e-6 || y < 1e-6) return false; +// Slow-rescaling helpers (lab frame, stationary nucleon target). +// E = primary neutrino lab energy +// M = target nucleon mass +// mc = charm-quark mass +inline double slowRescalingQ2(double xi, double y, double E, double M, double mc) { + return 2.0 * M * E * y * xi - mc * mc; +} +inline double xiToBjorkenX(double xi, double y, double E, double M, double mc) { + return xi - (mc * mc) / (2.0 * M * E * y); +} +inline double slowRescalingW2(double xi, double y, double E, double M, double mc) { + // W^2 = M^2 + 2 M E y (1 - xi) + m_c^2 = M^2 + (Q^2 + m_c^2)/xi - Q^2 + return M * M + 2.0 * M * E * y * (1.0 - xi) + mc * mc; +} + +///\brief Slow-rescaling kinematic check. +/// +/// Replaces the old (x_BJ, y) check. Cuts: +/// xi in [1e-9, 1], y in [1e-9, 1 - m_lep/E], +/// Q^2 = 2 M E y xi - m_c^2 > 0 (charm threshold), +/// W^2 = M^2 + 2 M E y (1-xi) + m_c^2 > (M + M_D0)^2. +bool kinematicallyAllowed(double xi, double y, double E, double M, double m_lep) { + if (xi < 1e-9 || xi > 1.0) return false; + if (y < 1e-9) return false; + if (y > 1.0 - m_lep / E) return false; + + const double mc = siren::utilities::Constants::CharmMass; + const double Mch = siren::utilities::Constants::D0Mass; - //denominator of a and b - double d = 2 * (1 + (M * x) / (2 * E)); - //the numerator of a (or a*d) - double ad = 1 - m * m * ((1 / (2 * M * E * x)) + (1 / (2 * E * E))); - double term = 1 - ((m * m) / (2 * M * E * x)); - //the numerator of b (or b*d) - double bd = sqrt(term * term - ((m * m) / (E * E))); + const double Q2 = slowRescalingQ2(xi, y, E, M, mc); + if (Q2 <= 0.0) return false; - double s = 2 * M * E; - double Q2 = s * x * y; - double Mc = siren::utilities::Constants::D0Mass; - return ((ad - bd) <= d * y and d * y <= (ad + bd)) && (Q2 * (1 - x) / x + pow(M, 2) >= pow(M + Mc, 2)); //Eq. 7 + const double W2 = slowRescalingW2(xi, y, E, M, mc); + if (W2 <= (M + Mch) * (M + Mch)) return false; + + return true; } } @@ -500,53 +507,56 @@ double QuarkDISFromSpline::DifferentialCrossSection(dataclasses::InteractionReco // however p4 is not used in computation here so we should be fine... double Q2 = -q.dot(q); - double x, y; double lepton_mass = GetLeptonMass(interaction.signature.secondary_types[lepton_index]); - y = 1.0 - p2.dot(p3) / p2.dot(p1); - x = Q2 / (2.0 * p2.dot(q)); + const double mc = siren::utilities::Constants::CharmMass; + double y = 1.0 - p2.dot(p3) / p2.dot(p1); + // xi from inverting Q^2 = 2 M E y xi - m_c^2: + double xi = (Q2 + mc * mc) / (2.0 * primary_energy * target_mass_ * y); double log_energy = log10(primary_energy); - std::array coordinates{{log_energy, log10(x), log10(y)}}; + std::array coordinates{{log_energy, log10(xi), log10(y)}}; std::array centers; - if (Q2 < minimum_Q2_ || !kinematicallyAllowed(x, y, primary_energy, target_mass_, lepton_mass) + if (Q2 < minimum_Q2_ + || !kinematicallyAllowed(xi, y, primary_energy, target_mass_, lepton_mass) || !differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { double E1_lab = interaction.interaction_parameters.at("energy"); - double E2_lab = p2.e(); - x = interaction.interaction_parameters.at("bjorken_x"); - y = interaction.interaction_parameters.at("bjorken_y"); - Q2 = 2. * E1_lab * E2_lab * x * y; + // Fallback: trust stored xi (records sampled by this class always have bjorken_xi). + // If absent, std::out_of_range is intentional: this class is xi-y only. + xi = interaction.interaction_parameters.at("bjorken_xi"); + y = interaction.interaction_parameters.at("bjorken_y"); + Q2 = slowRescalingQ2(xi, y, E1_lab, target_mass_, mc); } - return DifferentialCrossSection(primary_energy, x, y, lepton_mass, Q2); + return DifferentialCrossSection(primary_energy, xi, y, lepton_mass, Q2); } -double QuarkDISFromSpline::DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2) const { +double QuarkDISFromSpline::DifferentialCrossSection(double energy, double xi, double y, double secondary_lepton_mass, double Q2) const { double log_energy = log10(energy); // check preconditions - if(log_energy < differential_cross_section_.lower_extent(0) - || log_energy>differential_cross_section_.upper_extent(0)) - { - return 0.0;} - if(x <= 0 || x >= 1) { + if (log_energy < differential_cross_section_.lower_extent(0) + || log_energy > differential_cross_section_.upper_extent(0)) { + return 0.0; + } + if (xi <= 0 || xi >= 1) { return 0.0; } - if(y <= 0 || y >= 1){ + if (y <= 0 || y >= 1) { return 0.0; } - if(std::isnan(Q2)) { - Q2 = 2.0 * energy * target_mass_ * x * y; + if (std::isnan(Q2)) { + Q2 = slowRescalingQ2(xi, y, energy, target_mass_, + siren::utilities::Constants::CharmMass); } - if(Q2 < minimum_Q2_) { + if (Q2 < minimum_Q2_) { return 0; - } // cross section not calculated, assumed to be zero - - if(!kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) { + } + if (!kinematicallyAllowed(xi, y, energy, target_mass_, secondary_lepton_mass)) { return 0; } - std::array coordinates{{log_energy, log10(x), log10(y)}}; + std::array coordinates{{log_energy, log10(xi), log10(y)}}; std::array centers; - if(!differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { + if (!differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { return 0; } double result = pow(10., differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0)); @@ -597,27 +607,37 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR double E1_lab = p1_lab.e(); double E2_lab = p2_lab.e(); - // The out-going particle always gets at least enough energy for its rest mass - double yMax = 1 - m / primary_energy; - double logYMax = log10(yMax); + const double mc = siren::utilities::Constants::CharmMass; + const double Mch = siren::utilities::Constants::D0Mass; + const double M_targ = target_mass_; + const double E_nu = primary_energy; + const double Q2_min = minimum_Q2_; + + const double yMax = 1.0 - m / E_nu; + const double W2_thr = (M_targ + Mch) * (M_targ + Mch); + const double yMin = (W2_thr - M_targ * M_targ + Q2_min) / (2.0 * M_targ * E_nu); + const double xiMin = (mc * mc + Q2_min) / (2.0 * M_targ * E_nu * yMax); + + if (xiMin >= 1.0 || yMin >= yMax) { + throw std::runtime_error( + "QuarkDISFromSpline: primary energy below slow-rescaling charm threshold " + "(xiMin=" + std::to_string(xiMin) + ", yMin=" + std::to_string(yMin) + + ", yMax=" + std::to_string(yMax) + ")"); + } - // The minimum allowed value of y occurs when x = 1 and Q is minimized - double yMin = minimum_Q2_ / (2 * E1_lab * E2_lab); - double logYMin = log10(yMin); - // The minimum allowed value of x occurs when y = yMax and Q is minimized - // double xMin = minimum_Q2_ / ((s - target_mass_ * target_mass_) * yMax); - double xMin = minimum_Q2_ / (2 * E1_lab * E2_lab * yMax); - double logXMin = log10(xMin); + const double logXiMin = std::log10(xiMin); + const double logYMin = std::log10(yMin); + const double logYMax = std::log10(yMax); bool accept; - // kin_vars and its twin are 3-vectors containing [nu-energy, Bjorken X, Bjorken Y] + // kin_vars and its twin are 3-vectors containing [nu-energy, xi, Bjorken Y] std::array kin_vars, test_kin_vars; // centers of the cross section spline tales. std::array spline_table_center, test_spline_table_center; - // values of cross_section from the splines. By * Bx * Spline(E,x,y) + // values of cross_section from the splines. By * Bxi * Spline(E,xi,y) double cross_section, test_cross_section; // No matter what, we're evaluating at this specific energy. @@ -638,10 +658,15 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR do { if (trials >= 100) throw std::runtime_error("too much trials"); trials += 1; - kin_vars[1] = random->Uniform(logXMin,0); - kin_vars[2] = random->Uniform(logYMin,logYMax); - trialQ = (2 * E1_lab * E2_lab) * pow(10., kin_vars[1] + kin_vars[2]); - } while(trialQUniform(logXiMin, 0.0); + kin_vars[2] = random->Uniform(logYMin, logYMax); + const double xi_trial = std::pow(10., kin_vars[1]); + const double y_trial = std::pow(10., kin_vars[2]); + trialQ = slowRescalingQ2(xi_trial, y_trial, E_nu, M_targ, mc); + } while (trialQ < minimum_Q2_ + || !kinematicallyAllowed(std::pow(10., kin_vars[1]), + std::pow(10., kin_vars[2]), + E_nu, M_targ, m)); accept = true; //sanity check: demand that the sampled point be within the table extents @@ -662,9 +687,9 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR } while(!accept); //TODO: better proposal distribution? - double measure = pow(10., kin_vars[1] + kin_vars[2]); // Bx * By + double measure = pow(10., kin_vars[1] + kin_vars[2]); // Bxi * By - // Bx * By * xs(E, x, y) + // Bxi * By * xs(E, xi, y) // evalutates the differential spline at that point cross_section = measure*pow(10., differential_cross_section_.ndsplineeval(kin_vars.data(), spline_table_center.data(), 0)); @@ -676,10 +701,15 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR // repeat the sampling from above to get a new valid point double trialQ; do { - test_kin_vars[1] = random->Uniform(logXMin, 0); + test_kin_vars[1] = random->Uniform(logXiMin, 0.0); test_kin_vars[2] = random->Uniform(logYMin, logYMax); - trialQ = (2 * E1_lab * E2_lab) * pow(10., test_kin_vars[1] + test_kin_vars[2]); - } while(trialQ < minimum_Q2_ || !kinematicallyAllowed(pow(10., test_kin_vars[1]), pow(10., test_kin_vars[2]), primary_energy, target_mass_, m)); + const double xi_trial = std::pow(10., test_kin_vars[1]); + const double y_trial = std::pow(10., test_kin_vars[2]); + trialQ = slowRescalingQ2(xi_trial, y_trial, E_nu, M_targ, mc); + } while (trialQ < minimum_Q2_ + || !kinematicallyAllowed(std::pow(10., test_kin_vars[1]), + std::pow(10., test_kin_vars[2]), + E_nu, M_targ, m)); accept = true; if(test_kin_vars[1] < differential_cross_section_.lower_extent(1) @@ -711,14 +741,18 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR } // scaling down to handle numerical issues - double final_x = pow(10., kin_vars[1]); + double final_xi = std::pow(10., kin_vars[1]); double final_y = pow(10., kin_vars[2]); record.interaction_parameters.clear(); record.interaction_parameters["energy"] = E1_lab; - record.interaction_parameters["bjorken_x"] = final_x; - record.interaction_parameters["bjorken_y"] = final_y; - - double Q2 = 2 * E1_lab * E2_lab * pow(10.0, kin_vars[1] + kin_vars[2]); + record.interaction_parameters["bjorken_xi"] = final_xi; + record.interaction_parameters["bjorken_y"] = final_y; + record.interaction_parameters["bjorken_x"] = + xiToBjorkenX(final_xi, final_y, E1_lab, target_mass_, + siren::utilities::Constants::CharmMass); + + double Q2 = slowRescalingQ2(final_xi, final_y, E1_lab, target_mass_, + siren::utilities::Constants::CharmMass); double p1x_lab = std::sqrt(p1_lab.px() * p1_lab.px() + p1_lab.py() * p1_lab.py() + p1_lab.pz() * p1_lab.pz()); double pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); double momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); @@ -733,7 +767,8 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR double p1_lab_z = p1_lab.pz(); // loop to resolve precision issue while (iteration <= maxIterations) { - Q2 = 2. * E1_lab * E2_lab * pow(10.0, kin_vars[1] + kin_vars[2]); + Q2 = slowRescalingQ2(final_xi, final_y, E1_lab, target_mass_, + siren::utilities::Constants::CharmMass); p1x_lab = std::sqrt(p1_lab_x * p1_lab_x + p1_lab_y * p1_lab_y + p1_lab_z * p1_lab_z); pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); @@ -787,10 +822,10 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR // New hadronization scheme: includes partonic cross section sampling and slow rescaling for charm mass effects // ############################################## - double m_c = siren::utilities::Constants::CharmMass; - double xi = final_x * (1.0 + m_c * m_c / Q2); + const double m_c = siren::utilities::Constants::CharmMass; + const double xi = final_xi; // sampled directly in slow-rescaling if (xi >= 1.0) { - throw(siren::utilities::InjectionFailure("xi >= 1.0; slow-rescaling pushed Bjorken-x past unity")); + throw(siren::utilities::InjectionFailure("xi >= 1.0; sampled slow-rescaling xi past unity")); } rk::P4 p_parton(geom3::Vector3(0, 0, 0), xi * target_mass_); // parton at rest: (ξM, 0, 0, 0) rk::P4 p4_lab = p_parton + pq_lab; // struck charm = ξ*p2 + q @@ -984,7 +1019,7 @@ std::vector QuarkDISFromSpline::GetPossibleSi } std::vector QuarkDISFromSpline::DensityVariables() const { - return std::vector{"Bjorken x", "Bjorken y"}; + return std::vector{"Bjorken xi", "Bjorken y"}; } } // namespace interactions diff --git a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h index 9c670ecb6..13d8912b0 100644 --- a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h +++ b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h @@ -36,6 +36,11 @@ namespace siren { namespace utilities { class SIREN_random; } } namespace siren { namespace interactions { +/// Slow-rescaling charm DIS sampler. Expects FITS splines tabulated as +/// dsdxidy(log10 E, log10 xi, log10 y), e.g. Maboi_M_Muon_SR/dsdxidy_*.fits. +/// Charm-quark mass m_c=1.27 GeV (Constants::CharmMass) and lightest charm +/// hadron M_D0=1.86962 GeV (Constants::D0Mass) are taken from +/// siren::utilities::Constants and are not configurable per-instance. class QuarkDISFromSpline : public CrossSection { friend cereal::access; private: @@ -48,7 +53,7 @@ friend cereal::access; std::map> targets_by_primary_types_; std::map, std::vector> signatures_by_parent_types_; std::set D_types_; - + // used by the DIS process int interaction_type_; double target_mass_; @@ -58,7 +63,7 @@ friend cereal::access; double fragmentation_integral = 0; // for storing the integrated unnormed pdf void normalize_pdf(); // for normalizing pdf and integral, to be called at initialization siren::utilities::Interpolator1D inverseCdfTable; // for storing the CDF-1 table for the hadronization - + double unit; public: @@ -69,7 +74,7 @@ friend cereal::access; QuarkDISFromSpline(std::string differential_filename, std::string total_filename, std::set primary_types, std::set target_types, std::string units = "cm"); QuarkDISFromSpline(std::string differential_filename, std::string total_filename, int interaction, double target_mass, double minumum_Q2, std::vector primary_types, std::vector target_types, std::string units = "cm"); QuarkDISFromSpline(std::string differential_filename, std::string total_filename, std::vector primary_types, std::vector target_types, std::string units = "cm"); - + void SetUnits(std::string units); void SetInteractionType(int interaction); @@ -80,7 +85,7 @@ friend cereal::access; double TotalCrossSection(siren::dataclasses::ParticleType primary, double energy) const; double TotalCrossSectionAllFinalStates(dataclasses::InteractionRecord const &) const override; double DifferentialCrossSection(dataclasses::InteractionRecord const &) const override; - double DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2=std::numeric_limits::quiet_NaN()) const; + double DifferentialCrossSection(double energy, double xi, double y, double secondary_lepton_mass, double Q2=std::numeric_limits::quiet_NaN()) const; double InteractionThreshold(dataclasses::InteractionRecord const &) const override; // function definitions needed to compute the hadronization vertex From e00a15e45d56b3510ac89ff99e1325153d9f08f0 Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Fri, 8 May 2026 15:13:37 -0400 Subject: [PATCH 38/93] Fix log10 guards, burn-in trial cap, and dead-code in slow-rescaling QuarkDIS Code-review fixes on top of 6adde21: - Guard log10 against non-positive xi/y in DifferentialCrossSection(InteractionRecord) by short-circuiting to fallback before any log10 call (avoids NaN propagating into spline searchcenters). - Guard log10 against non-positive xiMin/yMin/yMax in SampleFinalState bounds block with an early throw before log10 calls. - Add 100-trial cap to MH burn-in proposal loop (matches initial-sample loop). - Remove redundant pow(10,...) recomputation: lift xi_trial/y_trial above the do-while in both sampling loops and pass them directly to kinematicallyAllowed. - Drop dead E2_lab /= 10 and E2_lab *= pow(10,iteration) lines in the precision-rescue iteration block (Q2 no longer references E2_lab). - Remove redundant m_c declaration in hadronization block (mc already in scope). --- .../private/QuarkDISFromSpline.cxx | 61 +++++++++++-------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 887073597..1fb0ad499 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -511,21 +511,28 @@ double QuarkDISFromSpline::DifferentialCrossSection(dataclasses::InteractionReco const double mc = siren::utilities::Constants::CharmMass; double y = 1.0 - p2.dot(p3) / p2.dot(p1); - // xi from inverting Q^2 = 2 M E y xi - m_c^2: - double xi = (Q2 + mc * mc) / (2.0 * primary_energy * target_mass_ * y); + // xi from inverting Q^2 = 2 M E y xi - m_c^2; guard against y <= 0: + double xi = (y > 0.0) + ? (Q2 + mc * mc) / (2.0 * primary_energy * target_mass_ * y) + : 0.0; double log_energy = log10(primary_energy); - std::array coordinates{{log_energy, log10(xi), log10(y)}}; std::array centers; - if (Q2 < minimum_Q2_ - || !kinematicallyAllowed(xi, y, primary_energy, target_mass_, lepton_mass) - || !differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { - double E1_lab = interaction.interaction_parameters.at("energy"); - // Fallback: trust stored xi (records sampled by this class always have bjorken_xi). - // If absent, std::out_of_range is intentional: this class is xi-y only. - xi = interaction.interaction_parameters.at("bjorken_xi"); - y = interaction.interaction_parameters.at("bjorken_y"); - Q2 = slowRescalingQ2(xi, y, E1_lab, target_mass_, mc); + bool use_sample_kinematics = (xi > 0.0 && y > 0.0 && Q2 >= minimum_Q2_); + if (use_sample_kinematics) { + std::array coordinates{{log_energy, log10(xi), log10(y)}}; + use_sample_kinematics = + kinematicallyAllowed(xi, y, primary_energy, target_mass_, lepton_mass) + && differential_cross_section_.searchcenters(coordinates.data(), centers.data()); + } + + if (!use_sample_kinematics) { + double E1_lab = interaction.interaction_parameters.at("energy"); + // Fallback: trust stored xi (records sampled by this class always have bjorken_xi). + // If absent, std::out_of_range is intentional: this class is xi-y only. + xi = interaction.interaction_parameters.at("bjorken_xi"); + y = interaction.interaction_parameters.at("bjorken_y"); + Q2 = slowRescalingQ2(xi, y, E1_lab, target_mass_, mc); } return DifferentialCrossSection(primary_energy, xi, y, lepton_mass, Q2); } @@ -618,6 +625,12 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR const double yMin = (W2_thr - M_targ * M_targ + Q2_min) / (2.0 * M_targ * E_nu); const double xiMin = (mc * mc + Q2_min) / (2.0 * M_targ * E_nu * yMax); + if (xiMin <= 0.0 || yMin <= 0.0 || yMax <= 0.0) { + throw std::runtime_error( + "QuarkDISFromSpline: non-positive sampling-bound (xiMin=" + + std::to_string(xiMin) + ", yMin=" + std::to_string(yMin) + + ", yMax=" + std::to_string(yMax) + ")"); + } if (xiMin >= 1.0 || yMin >= yMax) { throw std::runtime_error( "QuarkDISFromSpline: primary energy below slow-rescaling charm threshold " @@ -655,18 +668,17 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR // rejection sample a point which is kinematically allowed by calculation limits double trialQ; double trials = 0; + double xi_trial = 0.0, y_trial = 0.0; do { if (trials >= 100) throw std::runtime_error("too much trials"); trials += 1; kin_vars[1] = random->Uniform(logXiMin, 0.0); kin_vars[2] = random->Uniform(logYMin, logYMax); - const double xi_trial = std::pow(10., kin_vars[1]); - const double y_trial = std::pow(10., kin_vars[2]); + xi_trial = std::pow(10., kin_vars[1]); + y_trial = std::pow(10., kin_vars[2]); trialQ = slowRescalingQ2(xi_trial, y_trial, E_nu, M_targ, mc); } while (trialQ < minimum_Q2_ - || !kinematicallyAllowed(std::pow(10., kin_vars[1]), - std::pow(10., kin_vars[2]), - E_nu, M_targ, m)); + || !kinematicallyAllowed(xi_trial, y_trial, E_nu, M_targ, m)); accept = true; //sanity check: demand that the sampled point be within the table extents @@ -700,16 +712,18 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR for(size_t j = 0; j <= burnin; j++) { // repeat the sampling from above to get a new valid point double trialQ; + double xi_trial = 0.0, y_trial = 0.0; + int burnin_trials = 0; do { + if (++burnin_trials >= 100) + throw std::runtime_error("QuarkDISFromSpline: burn-in proposal failed to find allowed point in 100 trials"); test_kin_vars[1] = random->Uniform(logXiMin, 0.0); test_kin_vars[2] = random->Uniform(logYMin, logYMax); - const double xi_trial = std::pow(10., test_kin_vars[1]); - const double y_trial = std::pow(10., test_kin_vars[2]); + xi_trial = std::pow(10., test_kin_vars[1]); + y_trial = std::pow(10., test_kin_vars[2]); trialQ = slowRescalingQ2(xi_trial, y_trial, E_nu, M_targ, mc); } while (trialQ < minimum_Q2_ - || !kinematicallyAllowed(std::pow(10., test_kin_vars[1]), - std::pow(10., test_kin_vars[2]), - E_nu, M_targ, m)); + || !kinematicallyAllowed(xi_trial, y_trial, E_nu, M_targ, m)); accept = true; if(test_kin_vars[1] < differential_cross_section_.lower_extent(1) @@ -775,7 +789,6 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR if (pqx_lab>momq_lab){ //scale down E1_lab /= 10; - E2_lab /= 10; p1_lab_x /= 10; p1_lab_y /= 10; p1_lab_z /= 10; @@ -791,7 +804,6 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR // //scale back if (iteration > 0) { E1_lab *= pow(10.0, iteration); - E2_lab *= pow(10.0, iteration); p1_lab_x *= pow(10.0, iteration); p1_lab_y *= pow(10.0, iteration); p1_lab_z *= pow(10.0, iteration); @@ -822,7 +834,6 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR // New hadronization scheme: includes partonic cross section sampling and slow rescaling for charm mass effects // ############################################## - const double m_c = siren::utilities::Constants::CharmMass; const double xi = final_xi; // sampled directly in slow-rescaling if (xi >= 1.0) { throw(siren::utilities::InjectionFailure("xi >= 1.0; sampled slow-rescaling xi past unity")); From 8a2f6b057ee8b582d81363837c2bde90a7027075 Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Fri, 8 May 2026 15:28:00 -0400 Subject: [PATCH 39/93] Add 100-event smoke test for slow-rescaling QuarkDIS sampling --- tests/slow_rescaling/smoke_quarkdis_100.py | 171 +++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 tests/slow_rescaling/smoke_quarkdis_100.py diff --git a/tests/slow_rescaling/smoke_quarkdis_100.py b/tests/slow_rescaling/smoke_quarkdis_100.py new file mode 100644 index 000000000..dff09a593 --- /dev/null +++ b/tests/slow_rescaling/smoke_quarkdis_100.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +""" +Smoke test for slow-rescaling (xi, y) sampling in QuarkDISFromSpline. +Exercises 100 events and verifies kinematic bounds for each. +""" +import sys +import traceback + +# --------------------------------------------------------------------------- +# Constants (mirror C++ values exactly) +# --------------------------------------------------------------------------- +M_C = 1.27 # Constants::CharmMass +M_D0 = 1.86962 # Constants::D0Mass +M_N = (0.938272 + 0.939565) / 2 # isoscalar nucleon mass +M_MU = 0.105658 # muon mass +Q2MIN = 1.0 # GeV^2 +N_EVENTS = 100 +E_NU = 100.0 # neutrino energy in GeV + +# Spline files +SPLINE_DIR = ( + "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/pzhelnin/" + "DiMuons/Simulation/Resources/Splines/Maboi_M_Muon_SR" +) +DIFF_FILE = SPLINE_DIR + "/dsdxidy_nu-N-cc-charm-CT14nlo_central.fits" +TOTAL_FILE = SPLINE_DIR + "/sigma_nu-N-cc-charm-CT14nlo_central.fits" + +# --------------------------------------------------------------------------- +# Import SIREN +# --------------------------------------------------------------------------- +try: + import siren + import siren.interactions + import siren.dataclasses + import siren.utilities +except Exception as exc: + print(f"IMPORT ERROR: {exc}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + sys.exit(1) + +PT = siren.dataclasses.Particle.ParticleType + +# --------------------------------------------------------------------------- +# Construct cross-section object +# --------------------------------------------------------------------------- +try: + xs = siren.interactions.QuarkDISFromSpline( + DIFF_FILE, + TOTAL_FILE, + int(1), # interaction_type: CC + int(1), # quark_type: +1 for nu -> c + M_N, # isoscalar mass + int(1), # minimum Q2 + [PT.NuMu], # primary types + [PT.O16Nucleus], # target types + "m", # units + ) +except Exception as exc: + print(f"CONSTRUCTOR ERROR: {exc}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + sys.exit(1) + +# --------------------------------------------------------------------------- +# Get a valid signature +# --------------------------------------------------------------------------- +try: + sigs = list(xs.GetPossibleSignatures()) + assert len(sigs) > 0, "QuarkDISFromSpline returned no signatures" + sig = sigs[0] +except Exception as exc: + print(f"SIGNATURE ERROR: {exc}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + sys.exit(1) + +# --------------------------------------------------------------------------- +# RNG +# --------------------------------------------------------------------------- +rng = siren.utilities.SIREN_random(1234) + +# --------------------------------------------------------------------------- +# Expected kinematic bounds (mirror C++ Step 5) +# --------------------------------------------------------------------------- +y_max = 1.0 - M_MU / E_NU +W2_thr = (M_N + M_D0) ** 2 +y_min = (W2_thr - M_N**2 + Q2MIN) / (2.0 * M_N * E_NU) +xi_min = (M_C**2 + Q2MIN) / (2.0 * M_N * E_NU * y_max) + +print(f"Expected bounds:") +print(f" y_min = {y_min:.6f}") +print(f" y_max = {y_max:.6f}") +print(f" xi_min = {xi_min:.6f}") +print(f" W2_thr = {W2_thr:.6f}") +print(f" Q2MIN = {Q2MIN:.6f}") +print() + +# --------------------------------------------------------------------------- +# Run N_EVENTS events +# --------------------------------------------------------------------------- +failures = [] + +for event_idx in range(N_EVENTS): + try: + ir = siren.dataclasses.InteractionRecord() + ir.signature = sig + ir.primary_momentum = [E_NU, 0.0, 0.0, E_NU] # massless nu along +z + ir.primary_mass = 0.0 + ir.target_mass = M_N + + cdr = siren.dataclasses.CrossSectionDistributionRecord(ir) + xs.SampleFinalState(cdr, rng) + + params = dict(cdr.interaction_parameters) + xi = params["bjorken_xi"] + y = params["bjorken_y"] + x = params["bjorken_x"] + + # Derived quantities + Q2 = 2.0 * M_N * E_NU * y * xi - M_C**2 + W2 = M_N**2 + 2.0 * M_N * E_NU * y * (1.0 - xi) + M_C**2 + + # Check all assertions + event_failures = [] + if not (xi_min <= xi <= 1.0): + event_failures.append( + f"xi={xi:.6g} not in [{xi_min:.6g}, 1.0]" + ) + if not (y_min <= y <= y_max): + event_failures.append( + f"y={y:.6g} not in [{y_min:.6g}, {y_max:.6g}]" + ) + if not (x > 0.0): + event_failures.append(f"x={x:.6g} not > 0") + if not (Q2 >= Q2MIN): + event_failures.append( + f"Q2={Q2:.6g} < Q2MIN={Q2MIN:.6g}" + ) + if not (W2 > W2_thr): + event_failures.append( + f"W2={W2:.6g} not > W2_thr={W2_thr:.6g}" + ) + + if event_failures: + msg = ( + f"Event {event_idx}: xi={xi:.6g} y={y:.6g} " + f"x={x:.6g} Q2={Q2:.6g} W2={W2:.6g} | " + + "; ".join(event_failures) + ) + failures.append(msg) + print(f"FAIL: {msg}") + + except Exception as exc: + msg = f"Event {event_idx}: unexpected exception: {exc}" + failures.append(msg) + print(f"FAIL: {msg}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + + if len(failures) >= 10: + print(f"Stopping early after {len(failures)} failures.") + break + +# --------------------------------------------------------------------------- +# Report +# --------------------------------------------------------------------------- +if failures: + n_fail = len(failures) + n_ok = N_EVENTS - n_fail + print(f"\nFAIL {n_ok}/{N_EVENTS} ({n_fail} events failed bounds checks)") + sys.exit(1) +else: + print(f"OK {N_EVENTS}/{N_EVENTS}") + sys.exit(0) From 163862b0a9460379f805e237d56b51d42d72a6be Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Fri, 8 May 2026 15:48:04 -0400 Subject: [PATCH 40/93] Add 10k-event smoke + spline re-evaluation check for slow-rescaling QuarkDIS --- tests/slow_rescaling/smoke_quarkdis_10k.py | 256 +++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 tests/slow_rescaling/smoke_quarkdis_10k.py diff --git a/tests/slow_rescaling/smoke_quarkdis_10k.py b/tests/slow_rescaling/smoke_quarkdis_10k.py new file mode 100644 index 000000000..11fe791c4 --- /dev/null +++ b/tests/slow_rescaling/smoke_quarkdis_10k.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +""" +Smoke test for slow-rescaling (xi, y) sampling in QuarkDISFromSpline. +Exercises 10000 events, verifies kinematic bounds for each, and +checks that the spline DifferentialCrossSection returns finite positive +values for >=95% of sampled events. + +Spline re-evaluation approach: fallback via explicit bjorken_xi/bjorken_y. +A fresh InteractionRecord is constructed with dummy (zero) secondary_momenta +so the C++ code reaches its fallback path (Q2=0 < Q2MIN triggers it), which +then reads xi and y from interaction_parameters. +""" +import sys +import math +import traceback + +# --------------------------------------------------------------------------- +# Constants (mirror C++ values exactly) +# --------------------------------------------------------------------------- +M_C = 1.27 # Constants::CharmMass +M_D0 = 1.86962 # Constants::D0Mass +M_N = (0.938272 + 0.939565) / 2 # isoscalar nucleon mass +M_MU = 0.105658 # muon mass +Q2MIN = 1.0 # GeV^2 +N_EVENTS = 10000 +E_NU = 100.0 # neutrino energy in GeV + +# Spline files +SPLINE_DIR = ( + "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/pzhelnin/" + "DiMuons/Simulation/Resources/Splines/Maboi_M_Muon_SR" +) +DIFF_FILE = SPLINE_DIR + "/dsdxidy_nu-N-cc-charm-CT14nlo_central.fits" +TOTAL_FILE = SPLINE_DIR + "/sigma_nu-N-cc-charm-CT14nlo_central.fits" + +# --------------------------------------------------------------------------- +# Import SIREN +# --------------------------------------------------------------------------- +try: + import siren + import siren.interactions + import siren.dataclasses + import siren.utilities +except Exception as exc: + print(f"IMPORT ERROR: {exc}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + sys.exit(1) + +PT = siren.dataclasses.Particle.ParticleType + +# --------------------------------------------------------------------------- +# Construct cross-section object +# --------------------------------------------------------------------------- +try: + xs = siren.interactions.QuarkDISFromSpline( + DIFF_FILE, + TOTAL_FILE, + int(1), # interaction_type: CC + int(1), # quark_type: +1 for nu -> c + M_N, # isoscalar mass + int(1), # minimum Q2 + [PT.NuMu], # primary types + [PT.O16Nucleus], # target types + "m", # units + ) +except Exception as exc: + print(f"CONSTRUCTOR ERROR: {exc}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + sys.exit(1) + +# --------------------------------------------------------------------------- +# Get a valid signature +# --------------------------------------------------------------------------- +try: + sigs = list(xs.GetPossibleSignatures()) + assert len(sigs) > 0, "QuarkDISFromSpline returned no signatures" + sig = sigs[0] + n_secondaries = len(sig.secondary_types) +except Exception as exc: + print(f"SIGNATURE ERROR: {exc}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + sys.exit(1) + +# --------------------------------------------------------------------------- +# RNG +# --------------------------------------------------------------------------- +rng = siren.utilities.SIREN_random(1234) + +# --------------------------------------------------------------------------- +# Expected kinematic bounds (mirror C++ Step 5) +# --------------------------------------------------------------------------- +y_max = 1.0 - M_MU / E_NU +W2_thr = (M_N + M_D0) ** 2 +y_min = (W2_thr - M_N**2 + Q2MIN) / (2.0 * M_N * E_NU) +xi_min = (M_C**2 + Q2MIN) / (2.0 * M_N * E_NU * y_max) + +print(f"Expected bounds:") +print(f" y_min = {y_min:.6f}") +print(f" y_max = {y_max:.6f}") +print(f" xi_min = {xi_min:.6f}") +print(f" W2_thr = {W2_thr:.6f}") +print(f" Q2MIN = {Q2MIN:.6f}") +print() + +# --------------------------------------------------------------------------- +# Run N_EVENTS events — kinematic checks + collect (xi, y) for spline eval +# --------------------------------------------------------------------------- +failures = [] +sampled_kinematics = [] # list of (event_idx, xi, y, x, Q2, W2) + +for event_idx in range(N_EVENTS): + try: + ir = siren.dataclasses.InteractionRecord() + ir.signature = sig + ir.primary_momentum = [E_NU, 0.0, 0.0, E_NU] # massless nu along +z + ir.primary_mass = 0.0 + ir.target_mass = M_N + + cdr = siren.dataclasses.CrossSectionDistributionRecord(ir) + xs.SampleFinalState(cdr, rng) + + params = dict(cdr.interaction_parameters) + xi = params["bjorken_xi"] + y = params["bjorken_y"] + x = params["bjorken_x"] + + # Derived quantities + Q2 = 2.0 * M_N * E_NU * y * xi - M_C**2 + W2 = M_N**2 + 2.0 * M_N * E_NU * y * (1.0 - xi) + M_C**2 + + # Record kinematics for spline re-evaluation + sampled_kinematics.append((event_idx, xi, y, x, Q2, W2)) + + # Check all assertions + event_failures = [] + if not (xi_min <= xi <= 1.0): + event_failures.append( + f"xi={xi:.6g} not in [{xi_min:.6g}, 1.0]" + ) + if not (y_min <= y <= y_max): + event_failures.append( + f"y={y:.6g} not in [{y_min:.6g}, {y_max:.6g}]" + ) + if not (x > 0.0): + event_failures.append(f"x={x:.6g} not > 0") + if not (Q2 >= Q2MIN): + event_failures.append( + f"Q2={Q2:.6g} < Q2MIN={Q2MIN:.6g}" + ) + if not (W2 > W2_thr): + event_failures.append( + f"W2={W2:.6g} not > W2_thr={W2_thr:.6g}" + ) + + if event_failures: + msg = ( + f"Event {event_idx}: xi={xi:.6g} y={y:.6g} " + f"x={x:.6g} Q2={Q2:.6g} W2={W2:.6g} | " + + "; ".join(event_failures) + ) + failures.append(msg) + if len(failures) <= 10: + print(f"FAIL: {msg}") + + except Exception as exc: + msg = f"Event {event_idx}: unexpected exception: {exc}" + failures.append(msg) + if len(failures) <= 10: + print(f"FAIL: {msg}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + +# --------------------------------------------------------------------------- +# Spline re-evaluation: fallback approach +# +# Construct a fresh InteractionRecord with: +# - dummy zero secondary_momenta (so the C++ code computes Q2=0 < Q2MIN, +# which triggers its fallback path to read xi/y from interaction_parameters) +# - explicit bjorken_xi / bjorken_y in interaction_parameters +# +# Note: the "preferred" approach (cdr.record) crashes because secondary_momenta +# is not populated by SampleFinalState on CrossSectionDistributionRecord. +# --------------------------------------------------------------------------- +positive_xs_values = [] +zero_or_nan_count = 0 +xs_eval_errors = 0 + +dummy_momenta = [[0.0, 0.0, 0.0, 0.0]] * n_secondaries +dummy_masses = [0.0] * n_secondaries + +for (event_idx, xi, y, x, Q2, W2) in sampled_kinematics: + try: + ir2 = siren.dataclasses.InteractionRecord() + ir2.signature = sig + ir2.primary_momentum = [E_NU, 0.0, 0.0, E_NU] + ir2.primary_mass = 0.0 + ir2.target_mass = M_N + ir2.secondary_momenta = dummy_momenta + ir2.secondary_masses = dummy_masses + ir2.interaction_parameters = { + "energy": E_NU, + "bjorken_xi": xi, + "bjorken_y": y, + } + val = xs.DifferentialCrossSection(ir2) + if math.isfinite(val) and val > 0.0: + positive_xs_values.append(val) + else: + zero_or_nan_count += 1 + except Exception as exc: + xs_eval_errors += 1 + +# --------------------------------------------------------------------------- +# Summary statistics +# --------------------------------------------------------------------------- +n_evaluated = len(sampled_kinematics) +positive_fraction = len(positive_xs_values) / n_evaluated if n_evaluated > 0 else 0.0 + +if positive_xs_values: + mean_log_xs = sum(math.log10(v) for v in positive_xs_values) / len(positive_xs_values) +else: + mean_log_xs = float("nan") + +print(f"mean_log_xs = {mean_log_xs:.6f}") +print(f"positive xs fraction = {positive_fraction:.6f}") +if xs_eval_errors > 0 or zero_or_nan_count > 0: + print(f" (xs eval errors: {xs_eval_errors}, zero/nan: {zero_or_nan_count})") + +# --------------------------------------------------------------------------- +# Assertions +# --------------------------------------------------------------------------- +all_failures = [] + +if len(failures) != 0: + all_failures.append(f"kinematic failures: {len(failures)}") + +if not (positive_fraction > 0.95): + all_failures.append( + f"positive xs fraction {positive_fraction:.4f} <= 0.95" + ) + +if not math.isfinite(mean_log_xs): + all_failures.append(f"mean_log_xs is not finite: {mean_log_xs}") + +# --------------------------------------------------------------------------- +# Report +# --------------------------------------------------------------------------- +if all_failures: + print(f"\nFAIL: " + "; ".join(all_failures)) + if failures: + print("First kinematic failures:") + for msg in failures[:10]: + print(f" {msg}") + sys.exit(1) +else: + print(f"OK {N_EVENTS}/{N_EVENTS}") + sys.exit(0) From be32bd1e0aff8fbcfd4d763e8a37cd6175cc514f Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Fri, 8 May 2026 16:01:25 -0400 Subject: [PATCH 41/93] Avoid rk::P4::m() FP-roundoff assertion on hadronic remnant The post-MH hadronic remnant p4X is built via P4 arithmetic (operator+/-) that resets the mass cache, forcing P4::m() to recompute msq = e^2 - p.lengthSquared() independently of dot(). The do-while above already accepts p4X based on dot() >= 0, but roundoff between dot() and lengthSquared() can be ~1e-15 in either direction, occasionally triggering the assert msq >= 0 inside m(). Fix: use the validated dot() result directly when setting the hadronic remnant mass, clipping at 0 for safety. The do-while remains the gatekeeper for accepting p4X. The new (xi, y) sampling reaches this edge case more frequently than the old (x, y) sampling did; observed crash at event 1569 of a deterministic 10k run with seed 1234. --- projects/interactions/private/QuarkDISFromSpline.cxx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 1fb0ad499..c38e6bf63 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -877,7 +877,12 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR lepton.SetMass(p3.m()); lepton.SetHelicity(record.primary_helicity); hadron.SetFourMomentum({p4X.e(), p4X.px(), p4X.py(), p4X.pz()}); - hadron.SetMass(p4X.m()); + // Use the already-validated dot() result instead of P4::m(), which + // recomputes msq from e^2 - p_.lengthSquared() and can disagree with + // dot() by FP roundoff (~1e-15) -- the assert in m() would then fire + // even though the do-while above accepted p4X. + const double p4X_msq = p4X.dot(p4X); + hadron.SetMass(p4X_msq > 0.0 ? std::sqrt(p4X_msq) : 0.0); hadron.SetHelicity(record.target_helicity); meson.SetFourMomentum({p4CH.e(), p4CH.px(), p4CH.py(), p4CH.pz()}); From 409dea82d31e6efaf99a802dfa5ee4e89477734c Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Sat, 9 May 2026 14:42:56 -0400 Subject: [PATCH 42/93] QuarkDISFromSpline: NaN-guard pqy_lab precision loop Initialize pqy_lab to quiet NaN before the pqx_lab>momq_lab scaling loop and throw InjectionFailure if pqy_lab is still NaN after the loop. The loop body has multiple early-exit branches (break statements) that can leave pqy_lab unwritten if iteration_break/iteration_attempt / E1_lab relationships pin the value below threshold without finding a fix; the old code then read uninitialized memory in the subsequent Eq_lab and final-state momentum construction. Convert the silent footgun into a clean exception that the injector can catch and reject the event. --- projects/interactions/private/QuarkDISFromSpline.cxx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index c38e6bf63..42da1b750 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -770,7 +770,8 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR double p1x_lab = std::sqrt(p1_lab.px() * p1_lab.px() + p1_lab.py() * p1_lab.py() + p1_lab.pz() * p1_lab.pz()); double pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); double momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); - double pqy_lab, Eq_lab; + double pqy_lab = std::numeric_limits::quiet_NaN(); + double Eq_lab; if (pqx_lab>momq_lab){ // if current setting does not work, start looping through scalings @@ -812,6 +813,10 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR } // pqy_lab = 0; } else {pqy_lab = std::sqrt(momq_lab*momq_lab - pqx_lab *pqx_lab);} + if (std::isnan(pqy_lab)) { + throw(siren::utilities::InjectionFailure( + "QuarkDISFromSpline::SampleFinalState: precision loop failed to converge; pqy_lab is NaN")); + } Eq_lab = E1_lab * final_y; geom3::UnitVector3 x_dir = geom3::UnitVector3::xAxis(); From 0a3dffd4d73ec4933b80b354871182bbd050d5d7 Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Sat, 9 May 2026 15:57:11 -0400 Subject: [PATCH 43/93] QuarkDISFromSpline: drop spurious TotalCrossSectionAllFinalStates override Header carried an override declaration that was a leftover from an older internal version of the class where the spline was treated as inclusive and the override returned it raw. With the per-signature fragmentation fraction now applied inside TotalCrossSection (Andys 3341768c), the correct behavior is the base class default: loop over the D-meson signatures and sum their per-signature TotalCrossSection. That gives spline x sum(fragfracs) = ~0.98 x spline (D0 + D+/- + Ds+/- = 0.6 + 0.23 + 0.15), with the small remainder corresponding to Lambda_c production that is not yet modeled. Dropping the override avoids a link-time unresolved symbol and lets the base default do the right thing. --- .../interactions/public/SIREN/interactions/QuarkDISFromSpline.h | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h index 13d8912b0..9a4952c6b 100644 --- a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h +++ b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h @@ -83,7 +83,6 @@ friend cereal::access; // function definitions needed to compute the DIS vertex double TotalCrossSection(dataclasses::InteractionRecord const &) const override; double TotalCrossSection(siren::dataclasses::ParticleType primary, double energy) const; - double TotalCrossSectionAllFinalStates(dataclasses::InteractionRecord const &) const override; double DifferentialCrossSection(dataclasses::InteractionRecord const &) const override; double DifferentialCrossSection(double energy, double xi, double y, double secondary_lepton_mass, double Q2=std::numeric_limits::quiet_NaN()) const; double InteractionThreshold(dataclasses::InteractionRecord const &) const override; From 0f0c8c42645ecb82c43922ec0f111b0edd97224b Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Sat, 9 May 2026 16:01:34 -0400 Subject: [PATCH 44/93] QuarkDISFromSpline: add Ds+/Ds- support Extend three helpers in QuarkDISFromSpline so Ds mesons are produced alongside D0 and D+/-: * FragmentationFraction: Ds+/Ds- = 0.15, matching the 0.60:0.23:0.15 thesis convention used in PythiaDISCrossSection and CharmHadronization. Sum across the three modeled D-types is 0.98; the residual ~0.02 corresponds to Lambda_c production not yet modeled. * DTypesForPrimary: add DsPlus to the nu set and DsMinus to the nubar set so InitializeSignatures generates symmetric c/c-bar Ds signatures. * getHadronMass: 1.96834 GeV (PDG 2022) for both DsPlus and DsMinus. Constants.h does not yet expose DsMass; literal is consistent with the Ds mass used in CharmMesonDecay::particleMass. isD() in dataclasses/Particle.cxx already recognizes Ds+/Ds- (added earlier in this branch), so the loop in TotalCrossSection that applies fragmentation fraction to D-meson signatures now picks up Ds without further changes. --- .../interactions/private/QuarkDISFromSpline.cxx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 42da1b750..1c1d307b3 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -235,6 +235,10 @@ double QuarkDISFromSpline::getHadronMass(siren::dataclasses::ParticleType hadron return( siren::utilities::Constants::DPlusMass); case siren::dataclasses::ParticleType::DMinus: return( siren::utilities::Constants::DPlusMass); + case siren::dataclasses::ParticleType::DsPlus: + return 1.96834; // GeV (PDG 2022); no Constants::DsMass yet + case siren::dataclasses::ParticleType::DsMinus: + return 1.96834; case siren::dataclasses::ParticleType::Charm: return( siren::utilities::Constants::CharmMass); case siren::dataclasses::ParticleType::CharmBar: @@ -309,12 +313,14 @@ std::set QuarkDISFromSpline::DTypesForPrimary( || primary == siren::dataclasses::ParticleType::NuMu || primary == siren::dataclasses::ParticleType::NuTau) { return {siren::dataclasses::Particle::ParticleType::D0, - siren::dataclasses::Particle::ParticleType::DPlus}; + siren::dataclasses::Particle::ParticleType::DPlus, + siren::dataclasses::Particle::ParticleType::DsPlus}; } else if (primary == siren::dataclasses::ParticleType::NuEBar || primary == siren::dataclasses::ParticleType::NuMuBar || primary == siren::dataclasses::ParticleType::NuTauBar) { return {siren::dataclasses::Particle::ParticleType::D0Bar, - siren::dataclasses::Particle::ParticleType::DMinus}; + siren::dataclasses::Particle::ParticleType::DMinus, + siren::dataclasses::Particle::ParticleType::DsMinus}; } else { throw std::runtime_error("DTypesForPrimary: Unknown neutrino primary type!"); } @@ -996,7 +1002,9 @@ double QuarkDISFromSpline::FragmentationFraction(siren::dataclasses::Particle::P return 0.6; } else if (secondary == siren::dataclasses::Particle::ParticleType::DPlus || secondary == siren::dataclasses::Particle::ParticleType::DMinus) { return 0.23; - } // D_s and Lambda^+ not yet implemented + } else if (secondary == siren::dataclasses::Particle::ParticleType::DsPlus || secondary == siren::dataclasses::Particle::ParticleType::DsMinus) { + return 0.15; + } // Lambda_c not yet implemented return 0; } From eb5efcde2ea68ca6d0466faa2e4907333cf45b44 Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Sat, 9 May 2026 16:02:56 -0400 Subject: [PATCH 45/93] Update hardcoded D0 mass references to PDG value (1.86484) After Andys 1110a2d2 (D0Mass / DPlusMass literal swap), Constants::D0Mass now correctly returns 1.86484 GeV (PDG D0). Three places in this branch hardcoded the previous wrong value 1.86962 (which actually corresponded to the D+ mass) for Constants::D0Mass: * QuarkDISFromSpline.h:42 - documentation comment * tests/slow_rescaling/smoke_quarkdis_100.py:13 - sampling constant * tests/slow_rescaling/smoke_quarkdis_10k.py:21 - sampling constant Update all three to 1.86484 so they match the post-fix Constants::D0Mass that the C++ side emits. --- .../interactions/public/SIREN/interactions/QuarkDISFromSpline.h | 2 +- tests/slow_rescaling/smoke_quarkdis_100.py | 2 +- tests/slow_rescaling/smoke_quarkdis_10k.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h index 9a4952c6b..66fde7224 100644 --- a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h +++ b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h @@ -39,7 +39,7 @@ namespace interactions { /// Slow-rescaling charm DIS sampler. Expects FITS splines tabulated as /// dsdxidy(log10 E, log10 xi, log10 y), e.g. Maboi_M_Muon_SR/dsdxidy_*.fits. /// Charm-quark mass m_c=1.27 GeV (Constants::CharmMass) and lightest charm -/// hadron M_D0=1.86962 GeV (Constants::D0Mass) are taken from +/// hadron M_D0=1.86484 GeV (Constants::D0Mass) are taken from /// siren::utilities::Constants and are not configurable per-instance. class QuarkDISFromSpline : public CrossSection { friend cereal::access; diff --git a/tests/slow_rescaling/smoke_quarkdis_100.py b/tests/slow_rescaling/smoke_quarkdis_100.py index dff09a593..fb4b9a09e 100644 --- a/tests/slow_rescaling/smoke_quarkdis_100.py +++ b/tests/slow_rescaling/smoke_quarkdis_100.py @@ -10,7 +10,7 @@ # Constants (mirror C++ values exactly) # --------------------------------------------------------------------------- M_C = 1.27 # Constants::CharmMass -M_D0 = 1.86962 # Constants::D0Mass +M_D0 = 1.86484 # Constants::D0Mass M_N = (0.938272 + 0.939565) / 2 # isoscalar nucleon mass M_MU = 0.105658 # muon mass Q2MIN = 1.0 # GeV^2 diff --git a/tests/slow_rescaling/smoke_quarkdis_10k.py b/tests/slow_rescaling/smoke_quarkdis_10k.py index 11fe791c4..876b008bd 100644 --- a/tests/slow_rescaling/smoke_quarkdis_10k.py +++ b/tests/slow_rescaling/smoke_quarkdis_10k.py @@ -18,7 +18,7 @@ # Constants (mirror C++ values exactly) # --------------------------------------------------------------------------- M_C = 1.27 # Constants::CharmMass -M_D0 = 1.86962 # Constants::D0Mass +M_D0 = 1.86484 # Constants::D0Mass M_N = (0.938272 + 0.939565) / 2 # isoscalar nucleon mass M_MU = 0.105658 # muon mass Q2MIN = 1.0 # GeV^2 From 9d32939c6e93407d3ca228c2a290d0f88d439360 Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Sat, 9 May 2026 17:00:07 -0400 Subject: [PATCH 46/93] smoke tests: drop removed quark_type ctor argument Andys upstream 3341768c removed the vestigial int quark_type parameter from all QuarkDISFromSpline ctor signatures. Our smoke tests still passed it (predating that cleanup), causing TypeError on construction. Drop the line. --- tests/slow_rescaling/smoke_quarkdis_100.py | 1 - tests/slow_rescaling/smoke_quarkdis_10k.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/slow_rescaling/smoke_quarkdis_100.py b/tests/slow_rescaling/smoke_quarkdis_100.py index fb4b9a09e..a6dee474c 100644 --- a/tests/slow_rescaling/smoke_quarkdis_100.py +++ b/tests/slow_rescaling/smoke_quarkdis_100.py @@ -48,7 +48,6 @@ DIFF_FILE, TOTAL_FILE, int(1), # interaction_type: CC - int(1), # quark_type: +1 for nu -> c M_N, # isoscalar mass int(1), # minimum Q2 [PT.NuMu], # primary types diff --git a/tests/slow_rescaling/smoke_quarkdis_10k.py b/tests/slow_rescaling/smoke_quarkdis_10k.py index 876b008bd..3341d1393 100644 --- a/tests/slow_rescaling/smoke_quarkdis_10k.py +++ b/tests/slow_rescaling/smoke_quarkdis_10k.py @@ -56,7 +56,6 @@ DIFF_FILE, TOTAL_FILE, int(1), # interaction_type: CC - int(1), # quark_type: +1 for nu -> c M_N, # isoscalar mass int(1), # minimum Q2 [PT.NuMu], # primary types From 24f298acc71a1c37e8e4e2093d3b654eb18dac28 Mon Sep 17 00:00:00 2001 From: Pavel Zhelnin Date: Sat, 9 May 2026 17:08:36 -0400 Subject: [PATCH 47/93] QuarkDIS precision loop + smoke test retry on NaN-guard InjectionFailure * QuarkDISFromSpline::SampleFinalState: bump precision-loop max iterations from 10 to 15. Empirically, 10 vs 15 produces the same 6/10000 rejection rate at this E_nu scale, but giving the loop a few more chances at marginal-kinematics events is cheap insurance. * tests/slow_rescaling/smoke_quarkdis_{100,10k}.py: on InjectionFailure from the NaN guard, resample with fresh RNG state and retry up to 100 times before treating the event slot as unrecoverable. Mirrors the SIREN Injector framework which retries InjectionFailure events automatically; smoke tests bypass the Injector and need to do it explicitly. Track and report: - total resample count (events that needed >= 1 retry) - unrecoverable count (retry budget exhausted) Result: 10k smoke OK 10000/10000 sampled with 6 resamples; 100 OK 100/100. No spurious test failures. --- .../private/QuarkDISFromSpline.cxx | 2 +- tests/slow_rescaling/smoke_quarkdis_100.py | 24 ++++++++-- tests/slow_rescaling/smoke_quarkdis_10k.py | 46 +++++++++++++++++-- 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 1c1d307b3..47d2659db 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -781,7 +781,7 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR if (pqx_lab>momq_lab){ // if current setting does not work, start looping through scalings - int maxIterations = 10; + int maxIterations = 15; int iteration = 0; double p1_lab_x = p1_lab.px(); double p1_lab_y = p1_lab.py(); diff --git a/tests/slow_rescaling/smoke_quarkdis_100.py b/tests/slow_rescaling/smoke_quarkdis_100.py index a6dee474c..14164966f 100644 --- a/tests/slow_rescaling/smoke_quarkdis_100.py +++ b/tests/slow_rescaling/smoke_quarkdis_100.py @@ -96,6 +96,9 @@ # Run N_EVENTS events # --------------------------------------------------------------------------- failures = [] +rejected = [] +total_retries = 0 +MAX_RETRIES = 100 for event_idx in range(N_EVENTS): try: @@ -105,8 +108,21 @@ ir.primary_mass = 0.0 ir.target_mass = M_N - cdr = siren.dataclasses.CrossSectionDistributionRecord(ir) - xs.SampleFinalState(cdr, rng) + # Retry on NaN-guard InjectionFailure (mirrors SIREN Injector behavior). + for retry in range(MAX_RETRIES + 1): + cdr = siren.dataclasses.CrossSectionDistributionRecord(ir) + try: + xs.SampleFinalState(cdr, rng) + if retry > 0: + total_retries += retry + break + except RuntimeError as exc: + if 'precision loop failed to converge' in str(exc): + if retry < MAX_RETRIES: + continue + rejected.append(event_idx) + raise + raise params = dict(cdr.interaction_parameters) xi = params["bjorken_xi"] @@ -166,5 +182,7 @@ print(f"\nFAIL {n_ok}/{N_EVENTS} ({n_fail} events failed bounds checks)") sys.exit(1) else: - print(f"OK {N_EVENTS}/{N_EVENTS}") + retry_info = f" with {total_retries} resamples" if total_retries > 0 else "" + rej_info = f", {len(rejected)} unrecoverable" if rejected else "" + print(f"OK {N_EVENTS - len(rejected)}/{N_EVENTS} sampled{retry_info}{rej_info}") sys.exit(0) diff --git a/tests/slow_rescaling/smoke_quarkdis_10k.py b/tests/slow_rescaling/smoke_quarkdis_10k.py index 3341d1393..3e25f7c4d 100644 --- a/tests/slow_rescaling/smoke_quarkdis_10k.py +++ b/tests/slow_rescaling/smoke_quarkdis_10k.py @@ -105,8 +105,12 @@ # Run N_EVENTS events — kinematic checks + collect (xi, y) for spline eval # --------------------------------------------------------------------------- failures = [] +rejected = [] # NaN-guard rejections from precision loop (expected, not failures) sampled_kinematics = [] # list of (event_idx, xi, y, x, Q2, W2) +MAX_RETRIES = 100 # max resamples per event slot when NaN guard fires +total_retries = 0 + for event_idx in range(N_EVENTS): try: ir = siren.dataclasses.InteractionRecord() @@ -115,8 +119,27 @@ ir.primary_mass = 0.0 ir.target_mass = M_N - cdr = siren.dataclasses.CrossSectionDistributionRecord(ir) - xs.SampleFinalState(cdr, rng) + # Retry loop mirrors SIREN Injector behavior: on InjectionFailure + # (precision-loop NaN guard), resample with new RNG state until success. + # Production injector framework retries automatically; direct + # SampleFinalState calls (like this smoke test) need to do it explicitly. + for retry in range(MAX_RETRIES + 1): + cdr = siren.dataclasses.CrossSectionDistributionRecord(ir) + try: + xs.SampleFinalState(cdr, rng) + if retry > 0: + total_retries += retry + break + except RuntimeError as exc: + if 'precision loop failed to converge' in str(exc): + if retry < MAX_RETRIES: + continue # retry with fresh RNG draws + rejected.append(event_idx) + raise # exhausted budget + raise + else: + rejected.append(event_idx) + raise RuntimeError(f'event {event_idx}: exhausted {MAX_RETRIES} retries') params = dict(cdr.interaction_parameters) xi = params["bjorken_xi"] @@ -161,6 +184,15 @@ if len(failures) <= 10: print(f"FAIL: {msg}") + except RuntimeError as exc: + if 'precision loop failed to converge' in str(exc): + rejected.append(event_idx) + continue + msg = f"Event {event_idx}: unexpected exception: {exc}" + failures.append(msg) + if len(failures) <= 10: + print(f"FAIL: {msg}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) except Exception as exc: msg = f"Event {event_idx}: unexpected exception: {exc}" failures.append(msg) @@ -243,6 +275,12 @@ # --------------------------------------------------------------------------- # Report # --------------------------------------------------------------------------- +if total_retries > 0: + print(f"Resampled (NaN guard retry): {total_retries} resamples across {N_EVENTS} events") + +if rejected: + print(f"Rejected (NaN guard, retry budget exhausted): {len(rejected)}/{N_EVENTS} events ({100.0*len(rejected)/N_EVENTS:.2f}%)") + if all_failures: print(f"\nFAIL: " + "; ".join(all_failures)) if failures: @@ -251,5 +289,7 @@ print(f" {msg}") sys.exit(1) else: - print(f"OK {N_EVENTS}/{N_EVENTS}") + retry_info = f" with {total_retries} resamples" if total_retries > 0 else "" + rej_info = f", {len(rejected)} unrecoverable" if rejected else "" + print(f"OK {len(sampled_kinematics)}/{N_EVENTS} sampled{retry_info}{rej_info}") sys.exit(0) From cd7366bf69f95a6671980ffb1776c753f90e6e93 Mon Sep 17 00:00:00 2001 From: pzhelnin Date: Sun, 10 May 2026 15:52:56 -0400 Subject: [PATCH 48/93] PythiaDISCrossSection: register correct-sign D mesons for antineutrino primaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InitializeSignatures unconditionally registered {D0, D+, Ds+} for both neutrino and antineutrino primaries. SampleFinalState mutates the signature's meson slot to whatever Pythia actually produced — for ν̄ charm-CC that is {D̄0, D-, Ds-}, which then did not match any registered signature. The weighter could not look the post-mutation signature back up and event_weight came out NaN on essentially every ν̄ event. Pick the CP-mirror set when the primary is NuEBar / NuMuBar / NuTauBar so registered signatures stay consistent with what SampleFinalState writes back. ν side unchanged. Verified: with the fix, NuMuBar Phase-0 produces 100% finite weights (20/20 in the smoke test, vs ~0/1000 before). --- .../private/PythiaDISCrossSection.cxx | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/projects/interactions/private/PythiaDISCrossSection.cxx b/projects/interactions/private/PythiaDISCrossSection.cxx index c41285653..50384eb1f 100644 --- a/projects/interactions/private/PythiaDISCrossSection.cxx +++ b/projects/interactions/private/PythiaDISCrossSection.cxx @@ -250,11 +250,25 @@ void PythiaDISCrossSection::InitializeSignatures() { // Hadron remnant signature.secondary_types.push_back(siren::dataclasses::ParticleType::Hadrons); - // Charmed meson types: D0, D+, Ds (Ds support added to match SIREN_outputs 0420 run). + // Charmed meson types. For ν the c quark fragments to D0/D+/Ds+; for ν̄ the + // c̄ quark fragments to D̄0/D-/Ds-. SampleFinalState writes Pythia's actual + // produced PID into the signature's meson slot, so the registered set must + // include the correct charge to keep weighter signature lookups in range + // (otherwise event_weight comes out NaN — see fix in this commit). // TODO: Add Lambda_c (4122) support. - D_types_ = {siren::dataclasses::ParticleType::D0, - siren::dataclasses::ParticleType::DPlus, - siren::dataclasses::ParticleType::DsPlus}; + bool is_antineutrino = + (primary_type == siren::dataclasses::ParticleType::NuEBar || + primary_type == siren::dataclasses::ParticleType::NuMuBar || + primary_type == siren::dataclasses::ParticleType::NuTauBar); + if (is_antineutrino) { + D_types_ = {siren::dataclasses::ParticleType::D0Bar, + siren::dataclasses::ParticleType::DMinus, + siren::dataclasses::ParticleType::DsMinus}; + } else { + D_types_ = {siren::dataclasses::ParticleType::D0, + siren::dataclasses::ParticleType::DPlus, + siren::dataclasses::ParticleType::DsPlus}; + } for (auto meson_type : D_types_) { dataclasses::InteractionSignature full_signature = signature; From 3842959deb4e368097bcf9671108eafe96a4617d Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Mon, 11 May 2026 21:19:02 -0400 Subject: [PATCH 49/93] CharmMesonDecay: restore physical PDG branching ratios for semileptonic modes Revert the dimuon-analysis-specific force-muonic convention (D+/D-/D0/D0Bar mu BR forced to 1.0, e and hadronic BRs forced to 0.0) introduced upstream 2025-04-09 and preserved through pavelzhelnin#3. Main MiaochenJin/SIREN ships PDG-physical BRs so downstream multi-cascade analyses can use SIREN charm without per-event reweighting by the inverse forced BR. Adds the Ds+ -> Hadrons e+ nu_e and Ds- -> Hadrons e- nu_ebar signature registrations omitted by Pavel (symmetric with the mu channels). The force-muonic mode remains available on pavelzhelnin/SIREN as a local patch for the dimuon analysis. Physical PDG values applied (CharmMesonDecay::BranchingRatio): - D+ / D-: e 0.1607, mu 0.176, hadrons 0.6633 - D0 / D0Bar: e 0.0649, mu 0.067, hadrons 0.8681 - Ds+ / Ds-: e 0.0654, mu 0.0654, hadrons 0.8692 --- .../interactions/private/CharmMesonDecay.cxx | 79 ++++++++++--------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx index f4b0e0ac4..47c549bf8 100644 --- a/projects/interactions/private/CharmMesonDecay.cxx +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -172,6 +172,9 @@ double CharmMesonDecay::TotalDecayWidthForFinalState(dataclasses::InteractionRec std::set hadrons_muplus_numu = {siren::dataclasses::Particle::ParticleType::Hadrons, siren::dataclasses::Particle::ParticleType::MuPlus, siren::dataclasses::Particle::ParticleType::NuMu}; + std::set hadrons_eplus_nue = {siren::dataclasses::Particle::ParticleType::Hadrons, + siren::dataclasses::Particle::ParticleType::EPlus, + siren::dataclasses::Particle::ParticleType::NuE}; std::set hadrons = {siren::dataclasses::Particle::ParticleType::Hadrons}; // Anti-flavor (c̄) decay-mode sets, sign-conjugated from the c modes above. std::set k0_eminus_nuebar = {siren::dataclasses::Particle::ParticleType::K0, @@ -189,56 +192,50 @@ double CharmMesonDecay::TotalDecayWidthForFinalState(dataclasses::InteractionRec std::set hadrons_muminus_numubar = {siren::dataclasses::Particle::ParticleType::Hadrons, siren::dataclasses::Particle::ParticleType::MuMinus, siren::dataclasses::Particle::ParticleType::NuMuBar}; + std::set hadrons_eminus_nuebar = {siren::dataclasses::Particle::ParticleType::Hadrons, + siren::dataclasses::Particle::ParticleType::EMinus, + siren::dataclasses::Particle::ParticleType::NuEBar}; if (primary == siren::dataclasses::Particle::ParticleType::DPlus) { tau = 1040 * (1e-15); - // MODIFIED 2025-04-09: Force all decays to muonic semileptonic channel - // to maximize dimuon statistics. Must multiply event weights by physical BR - // in post-processing (D+ -> mu: 0.176, D+ -> e: 0.1607, D+ -> hadrons: 0.6633). - // To restore physical BRs, uncomment the original lines below: - // if (secondaries == k0_eplus_nue) {branching_ratio = .1607;} - // else if (secondaries == k0_muplus_numu) {branching_ratio = .176;} - // else if (secondaries == hadrons) {branching_ratio = (1 - .1607 - .176);} - if (secondaries == k0_eplus_nue) {branching_ratio = 0.0;} - else if (secondaries == k0_muplus_numu) {branching_ratio = 1.0;} - else if (secondaries == hadrons) {branching_ratio = 0.0;} + // Physical PDG BRs for D+ semileptonic decay (2022 values). + // Pavel's downstream dimuon analysis uses a force-muonic variant + // (BR_mu = 1.0, BR_e = 0, BR_hadrons = 0); that lives on his fork. + if (secondaries == k0_eplus_nue) {branching_ratio = .1607;} + else if (secondaries == k0_muplus_numu) {branching_ratio = .176;} + else if (secondaries == hadrons) {branching_ratio = (1 - .1607 - .176);} } else if (primary == siren::dataclasses::Particle::ParticleType::DMinus) { - // CP-mirror of D+ (same lifetime, same forced muonic BR convention). + // CP-mirror of D+ (same lifetime, same physical PDG BRs). tau = 1040 * (1e-15); - if (secondaries == k0_eminus_nuebar) {branching_ratio = 0.0;} - else if (secondaries == k0_muminus_numubar) {branching_ratio = 1.0;} - else if (secondaries == hadrons) {branching_ratio = 0.0;} + if (secondaries == k0_eminus_nuebar) {branching_ratio = .1607;} + else if (secondaries == k0_muminus_numubar) {branching_ratio = .176;} + else if (secondaries == hadrons) {branching_ratio = (1 - .1607 - .176);} } else if (primary == siren::dataclasses::Particle::ParticleType::D0) { tau = 410.1 * (1e-15); - // MODIFIED 2025-04-09: Force all decays to muonic semileptonic channel - // to maximize dimuon statistics. Must multiply event weights by physical BR - // in post-processing (D0 -> mu: 0.067, D0 -> e: 0.0649, D0 -> hadrons: 0.8681). - // To restore physical BRs, uncomment the original lines below: - // if (secondaries == kminus_eplus_nue) {branching_ratio = .0649;} - // else if (secondaries == kminus_muplus_numu) {branching_ratio = .067;} - // else if (secondaries == hadrons) {branching_ratio = (1 - .0649 - .067);} - if (secondaries == kminus_eplus_nue) {branching_ratio = 0.0;} - else if (secondaries == kminus_muplus_numu) {branching_ratio = 1.0;} - else if (secondaries == hadrons) {branching_ratio = 0.0;} + // Physical PDG BRs for D0 semileptonic decay (2022 values). + if (secondaries == kminus_eplus_nue) {branching_ratio = .0649;} + else if (secondaries == kminus_muplus_numu) {branching_ratio = .067;} + else if (secondaries == hadrons) {branching_ratio = (1 - .0649 - .067);} } else if (primary == siren::dataclasses::Particle::ParticleType::D0Bar) { - // CP-mirror of D0. + // CP-mirror of D0 (same lifetime, same physical PDG BRs). tau = 410.1 * (1e-15); - if (secondaries == kplus_eminus_nuebar) {branching_ratio = 0.0;} - else if (secondaries == kplus_muminus_numubar) {branching_ratio = 1.0;} - else if (secondaries == hadrons) {branching_ratio = 0.0;} + if (secondaries == kplus_eminus_nuebar) {branching_ratio = .0649;} + else if (secondaries == kplus_muminus_numubar) {branching_ratio = .067;} + else if (secondaries == hadrons) {branching_ratio = (1 - .0649 - .067);} } else if (primary == siren::dataclasses::Particle::ParticleType::DsPlus) { tau = 504 * (1e-15); // Ds+ lifetime: 504 fs (PDG) - // Force BR=1.0 for the muonic semileptonic channel (Ds -> Hadrons mu nu), - // matching the D+/D0 convention. Multiply by physical inclusive BR - // (Ds -> mu X ~ 0.0654, no tau feed-down) in post-processing. + // Physical PDG BRs for Ds+ semileptonic decay. Inclusive + // Ds -> e X = 0.0654, Ds -> mu X = 0.0654 (no tau feed-down). // Daughter "Hadrons" stands in for eta / eta' / phi (sampled in // SampleFinalState with fractions 0.46 / 0.16 / 0.38). - if (secondaries == hadrons_muplus_numu) {branching_ratio = 1.0;} - else if (secondaries == hadrons) {branching_ratio = 0.0;} + if (secondaries == hadrons_eplus_nue) {branching_ratio = .0654;} + else if (secondaries == hadrons_muplus_numu) {branching_ratio = .0654;} + else if (secondaries == hadrons) {branching_ratio = (1 - 2 * .0654);} } else if (primary == siren::dataclasses::Particle::ParticleType::DsMinus) { // CP-mirror of Ds+ (eta/eta'/phi are self-conjugate, only lepton/nu flip). tau = 504 * (1e-15); - if (secondaries == hadrons_muminus_numubar) {branching_ratio = 1.0;} - else if (secondaries == hadrons) {branching_ratio = 0.0;} + if (secondaries == hadrons_eminus_nuebar) {branching_ratio = .0654;} + else if (secondaries == hadrons_muminus_numubar) {branching_ratio = .0654;} + else if (secondaries == hadrons) {branching_ratio = (1 - 2 * .0654);} } else { std::cout << "this decay mode is not yet implemented!" << std::endl; @@ -319,12 +316,14 @@ std::vector CharmMesonDecay::GetPossibleSigna hadron_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; signatures.push_back(hadron_signature); } else if (primary==siren::dataclasses::Particle::ParticleType::DsPlus) { - // Ds: only the muonic semileptonic channel (Ds -> Hadrons mu nu) plus - // the all-hadronic catch-all. No e+ ve channel — would just sit at BR=0 - // anyway under the current "force-muonic" convention. + // Ds: muonic + electronic semileptonic channels plus all-hadronic catch-all. // The Hadrons daughter stands in for eta / eta' / phi (sampled inline // in SampleFinalState; SIREN ParticleType has no entries for these). semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; + semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::EPlus; + semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuE; + signatures.push_back(semilep_signature); + semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::MuPlus; semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuMu; signatures.push_back(semilep_signature); @@ -333,6 +332,10 @@ std::vector CharmMesonDecay::GetPossibleSigna } else if (primary==siren::dataclasses::Particle::ParticleType::DsMinus) { // CP-mirror of Ds+: eta/eta'/phi are self-conjugate, only lepton/nu flip. semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; + semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::EMinus; + semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuEBar; + signatures.push_back(semilep_signature); + semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::MuMinus; semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuMuBar; signatures.push_back(semilep_signature); From 52fa76d1fe6d3b369633b13fd4fd9b8718c18e2b Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Wed, 20 May 2026 09:30:39 -0400 Subject: [PATCH 50/93] address PR#74 comments --- .../dataclasses/private/InteractionRecord.cxx | 12 -- projects/dataclasses/private/Particle.cxx | 9 - .../public/SIREN/dataclasses/Particle.h | 2 - .../SIREN/dataclasses/ParticleTypes.def | 7 +- projects/detector/private/DetectorModel.cxx | 4 +- .../public/SIREN/detector/DetectorModel.h | 3 + .../SecondaryBoundedVertexDistribution.cxx | 17 -- .../SecondaryPhysicalVertexDistribution.cxx | 16 -- projects/injection/private/Injector.cxx | 172 +++++++----------- projects/injection/private/Weighter.cxx | 39 ---- projects/injection/private/WeightingUtils.cxx | 14 -- .../injection/ColumnDepthLeptonInjector.h | 1 - .../injection/CylinderVolumeLeptonInjector.h | 1 - .../injection/DecayRangeLeptonInjector.h | 1 - .../SIREN/injection/RangedLeptonInjector.h | 1 - .../public/SIREN/injection/Weighter.tcc | 41 ++--- projects/interactions/CMakeLists.txt | 1 - .../interactions/private/Hadronization.cxx | 17 -- .../private/InteractionCollection.cxx | 13 -- .../private/QuarkDISFromSpline.cxx | 4 - .../private/pybindings/Hadronization.h | 34 ---- .../pybindings/InteractionCollection.h | 6 - .../private/pybindings/interactions.cxx | 3 - .../public/SIREN/interactions/Hadronization.h | 58 ------ .../interactions/InteractionCollection.h | 10 - 25 files changed, 87 insertions(+), 399 deletions(-) delete mode 100644 projects/interactions/private/Hadronization.cxx delete mode 100644 projects/interactions/private/pybindings/Hadronization.h delete mode 100644 projects/interactions/public/SIREN/interactions/Hadronization.h diff --git a/projects/dataclasses/private/InteractionRecord.cxx b/projects/dataclasses/private/InteractionRecord.cxx index d74f8e3a9..2a379316f 100644 --- a/projects/dataclasses/private/InteractionRecord.cxx +++ b/projects/dataclasses/private/InteractionRecord.cxx @@ -625,15 +625,11 @@ void PrimaryDistributionRecord::FinalizeAvailable(InteractionRecord & record) co } void PrimaryDistributionRecord::Finalize(InteractionRecord & record) const { - //std::cout << "starting finalize" << std::endl; - //std::cout << record.signature << std::endl; - //std::cout << record.primary_momentum[0] << std::endl; record.signature.primary_type = type; record.primary_id = GetID(); record.interaction_vertex = GetInteractionVertex(); record.primary_initial_position = GetInitialPosition(); - //std::cout << "in primary record finalize" << std::endl; record.primary_mass = GetMass(); record.primary_momentum = GetFourMomentum(); record.primary_helicity = GetHelicity(); @@ -859,15 +855,12 @@ void SecondaryParticleRecord::UpdateMomentum() const { } void SecondaryParticleRecord::Finalize(InteractionRecord & record) const { - ////std::cout << "SecPartRecord::Finalize : starting for " << type << std::endl; assert(record.signature.secondary_types.at(secondary_index) == type); record.secondary_ids.at(secondary_index) = GetID(); record.secondary_masses.at(secondary_index) = GetMass(); record.secondary_momenta.at(secondary_index) = GetFourMomentum(); record.secondary_helicities.at(secondary_index) = GetHelicity(); - ////std::cout << "SecPartRecord::Finalize : finished for " << type << " with" << std::endl; - ////std::cout << record << std::endl; } @@ -987,10 +980,6 @@ SecondaryParticleRecord const & CrossSectionDistributionRecord::GetSecondaryPart } void CrossSectionDistributionRecord::Finalize(InteractionRecord & record) const { - //std::cout << "XsecDistRecord::Finalize : starting" << std::endl; - //std::cout << record.signature << std::endl; - //std::cout << signature << std::endl; - //std::cout << primary_momentum[0] << std::endl; record.target_id = target_id; record.target_mass = target_mass; @@ -1006,7 +995,6 @@ void CrossSectionDistributionRecord::Finalize(InteractionRecord & record) const record.secondary_helicities.resize(secondary_particles.size()); for(SecondaryParticleRecord const & secondary : secondary_particles) { - //std::cout << "XsecDistRecord::Finalize : going to secondaries: " << secondary << std::endl; secondary.Finalize(record); } } diff --git a/projects/dataclasses/private/Particle.cxx b/projects/dataclasses/private/Particle.cxx index 5a55fe480..154be3282 100644 --- a/projects/dataclasses/private/Particle.cxx +++ b/projects/dataclasses/private/Particle.cxx @@ -96,15 +96,6 @@ bool isCharged(ParticleType p){ p==ParticleType::Hadrons); } -bool isQuark(Particle::ParticleType p){ - return(p==Particle::ParticleType::Charm || p==Particle::ParticleType::CharmBar); - } - -bool isHadron(Particle::ParticleType p){ - return(p==Particle::ParticleType::Hadrons || - p==Particle::ParticleType::D0 || p==Particle::ParticleType::D0Bar || - p==Particle::ParticleType::DPlus || p==Particle::ParticleType::DMinus); - } bool isD(Particle::ParticleType p){ return(p==Particle::ParticleType::D0 || p==Particle::ParticleType::D0Bar || diff --git a/projects/dataclasses/public/SIREN/dataclasses/Particle.h b/projects/dataclasses/public/SIREN/dataclasses/Particle.h index 4f946a6dc..c4f12f24f 100644 --- a/projects/dataclasses/public/SIREN/dataclasses/Particle.h +++ b/projects/dataclasses/public/SIREN/dataclasses/Particle.h @@ -78,8 +78,6 @@ class Particle { bool isLepton(ParticleType p); bool isCharged(ParticleType p); bool isNeutrino(ParticleType p); -bool isQuark(Particle::ParticleType p); -bool isHadron(Particle::ParticleType p); bool isD(Particle::ParticleType p); diff --git a/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def b/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def index 81af84647..800899b97 100644 --- a/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def +++ b/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def @@ -32,6 +32,8 @@ X(Neutron, 2112) X(PPlus, 2212) X(PMinus, -2212) X(K0_Short, 310) +X(K0, 311) +X(K0Bar, -311) X(Eta, 221) X(EtaPrime, 331) X(Omega, 223) @@ -72,10 +74,6 @@ X(TauPlus, -15) X(TauMinus, 15) X(NuTau, 16) X(NuTauBar, -16) -X(Charm, 4) -X(CharmBar, -4) -X(K0, 311) -X(K0Bar, -311) @@ -212,7 +210,6 @@ X(NuclInt, -2000001004) X(MuPair, -2000001005) X(Hadrons, -2000001006) X(Decay, -2000001007) -X(Hadronization, 99) X(ContinuousEnergyLoss, -2000001111) X(FiberLaser, -2000002100) X(N2Laser, -2000002101) diff --git a/projects/detector/private/DetectorModel.cxx b/projects/detector/private/DetectorModel.cxx index 41ebb84be..5a740a8b9 100644 --- a/projects/detector/private/DetectorModel.cxx +++ b/projects/detector/private/DetectorModel.cxx @@ -710,7 +710,7 @@ double DetectorModel::GetInteractionDensity(Geometry::IntersectionList const & i std::vector const & total_cross_sections, double const & total_decay_length) const { Vector3D direction = p0 - intersections.position; - if(direction.magnitude() == 0 || direction.magnitude() <= 1e-5) { + if(direction.magnitude() == 0 || direction.magnitude() <= distance_threshold) { direction = intersections.direction; } else { direction.normalize(); @@ -1020,7 +1020,7 @@ double DetectorModel::GetInteractionDepthInCGS(Geometry::IntersectionList const if(distance == 0.0) { return 0.0; } - if(direction.magnitude() <= 1e-5) { + if(direction.magnitude() <= distance_threshold) { direction = intersections.direction; // return 1e-05; // I have to do this to ensure that it works } diff --git a/projects/detector/public/SIREN/detector/DetectorModel.h b/projects/detector/public/SIREN/detector/DetectorModel.h index d92625a6c..515fdab05 100644 --- a/projects/detector/public/SIREN/detector/DetectorModel.h +++ b/projects/detector/public/SIREN/detector/DetectorModel.h @@ -72,6 +72,9 @@ friend siren::detector::Path; math::Vector3D detector_origin_; math::Quaternion detector_rotation_; public: + // Threshold below which a direction vector's magnitude is treated as zero + // for the purpose of falling back to the intersection direction. + constexpr static double distance_threshold = 1e-5; DetectorModel(); DetectorModel(std::string const & detector_model, std::string const & material_model); DetectorModel(std::string const & path, std::string const & detector_model, std::string const & material_model); diff --git a/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx b/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx index 55bdd786e..86e438692 100644 --- a/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx +++ b/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx @@ -57,11 +57,6 @@ void SecondaryBoundedVertexDistribution::SampleVertex(std::shared_ptrHasHadronizations()) { - record.SetLength(0); - return; - } siren::math::Vector3D endcap_1 = endcap_0 + max_length * dir; siren::detector::Path path(detector_model, DetectorPosition(endcap_0), DetectorDirection(dir), max_length); @@ -126,14 +121,6 @@ double SecondaryBoundedVertexDistribution::GenerationProbability(std::shared_ptr siren::math::Vector3D vertex(record.interaction_vertex); siren::math::Vector3D endcap_0 = record.primary_initial_position; - // hadrnoization treated differently, but also check for misconducting - if (interactions->HasHadronizations()) { - if (vertex == endcap_0) { - return 1.0; - } else { - return 0.0; - } - } siren::math::Vector3D endcap_1 = endcap_0 + max_length * dir; siren::detector::Path path(detector_model, DetectorPosition(endcap_0), DetectorDirection(dir), max_length); @@ -188,10 +175,6 @@ double SecondaryBoundedVertexDistribution::GenerationProbability(std::shared_ptr prob_density = interaction_density * exp(-log_one_minus_exp_of_negative(total_interaction_depth) - traversed_interaction_depth); } - if (prob_density == 0) { - std::cout << "observed prob density 0 in physical vertex under process " << record.signature.primary_type << " to " - << record.signature.secondary_types[0] << " with total depth " << total_interaction_depth << std::endl; - } return prob_density; } diff --git a/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx b/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx index c758fc3ba..b25455125 100644 --- a/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx +++ b/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx @@ -55,11 +55,6 @@ void SecondaryPhysicalVertexDistribution::SampleVertex(std::shared_ptrHasHadronizations()) { - record.SetLength(0); - return; - } siren::detector::Path path(detector_model, DetectorPosition(endcap_0), DetectorDirection(dir), std::numeric_limits::infinity()); path.ClipToOuterBounds(); @@ -105,13 +100,6 @@ double SecondaryPhysicalVertexDistribution::GenerationProbability(std::shared_pt siren::math::Vector3D vertex(record.interaction_vertex); siren::math::Vector3D endcap_0 = record.primary_initial_position; - if (interactions->HasHadronizations()) { - if (vertex == endcap_0) { - return 1.0; - } else { - return 0.0; - } - } siren::detector::Path path(detector_model, DetectorPosition(endcap_0), DetectorDirection(dir), std::numeric_limits::infinity()); path.ClipToOuterBounds(); @@ -148,10 +136,6 @@ double SecondaryPhysicalVertexDistribution::GenerationProbability(std::shared_pt prob_density = interaction_density * exp(-log_one_minus_exp_of_negative(total_interaction_depth) - traversed_interaction_depth); } - if (prob_density == 0) { - std::cout << "observed prob density 0 in physical vertex under process " << record.signature.primary_type << " to " - << record.signature.secondary_types[0] << " with total depth " << total_interaction_depth << std::endl; - } return prob_density; } diff --git a/projects/injection/private/Injector.cxx b/projects/injection/private/Injector.cxx index 77a251418..f352ca9c0 100644 --- a/projects/injection/private/Injector.cxx +++ b/projects/injection/private/Injector.cxx @@ -12,7 +12,6 @@ #include "SIREN/interactions/DarkNewsCrossSection.h" #include "SIREN/interactions/InteractionCollection.h" #include "SIREN/interactions/Decay.h" -#include "SIREN/interactions/Hadronization.h" #include "SIREN/dataclasses/DecaySignature.h" #include "SIREN/dataclasses/InteractionSignature.h" @@ -194,122 +193,85 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record std::vector matching_signatures; std::vector> matching_cross_sections; std::vector> matching_decays; - std::vector> matching_hadronizations; siren::dataclasses::InteractionRecord fake_record = record; double fake_prob; - // if contains hadronization, then perform only hadronization - if (interactions->HasHadronizations()) { - double total_frag_prob = 0; - std::vector frag_probs; - for(auto const & hadronization : interactions->GetHadronizations() ) { - for(auto const & signature : hadronization->GetPossibleSignaturesFromParent(record.signature.primary_type)) { - double frag_prob = 0; - - fake_record.signature = signature; - for (auto & secondary : fake_record.signature.secondary_types) { - frag_prob += hadronization->FragmentationFraction(secondary); - } - - total_frag_prob += frag_prob; - frag_probs.push_back(total_frag_prob); - // Add target and decay pointer to the lists - matching_targets.push_back(siren::dataclasses::Particle::ParticleType::Decay); - matching_hadronizations.push_back(hadronization); - matching_signatures.push_back(signature); - } - } - - // now choose the specific charmed hadron to fragment into - double r = random->Uniform(0, total_frag_prob); - unsigned int index = 0; - for(; (index < frag_probs.size()-1) and (r > frag_probs[index]); ++index) { - } // fixes the index of the chosen fragmentation - record.signature.target_type = matching_targets[index]; - record.signature = matching_signatures[index]; - record.target_mass = detector_model->GetTargetMass(record.signature.target_type); - siren::dataclasses::CrossSectionDistributionRecord xsec_record(record); - - matching_hadronizations[index]->SampleFinalState(xsec_record, random); - xsec_record.Finalize(record); - } else { - if (interactions->HasCrossSections()) { - for(auto const target : available_targets) { - if(possible_targets.find(target) != possible_targets.end()) { - // Get target density - double target_density = detector_model->GetParticleDensity(intersections, DetectorPosition(interaction_vertex), target); - // Loop over cross sections that have this target - std::vector> const & target_cross_sections = interactions->GetCrossSectionsForTarget(target); - for(auto const & cross_section : target_cross_sections) { - // Loop over cross section signatures with the same target - std::vector signatures = cross_section->GetPossibleSignaturesFromParents(record.signature.primary_type, target); - for(auto const & signature : signatures) { - fake_record.signature = signature; - fake_record.target_mass = detector_model->GetTargetMass(target); - // Add total cross section times density to the total prob - fake_prob = target_density * cross_section->TotalCrossSection(fake_record); - total_prob += fake_prob; - xsec_prob += fake_prob; - // Add total prob to probs - probs.push_back(total_prob); - // Add target and cross section pointer to the lists - matching_targets.push_back(target); - matching_cross_sections.push_back(cross_section); - matching_signatures.push_back(signature); - } + if (interactions->HasCrossSections()) { + for(auto const target : available_targets) { + if(possible_targets.find(target) != possible_targets.end()) { + // Get target density + double target_density = detector_model->GetParticleDensity(intersections, DetectorPosition(interaction_vertex), target); + // Loop over cross sections that have this target + std::vector> const & target_cross_sections = interactions->GetCrossSectionsForTarget(target); + for(auto const & cross_section : target_cross_sections) { + // Loop over cross section signatures with the same target + std::vector signatures = cross_section->GetPossibleSignaturesFromParents(record.signature.primary_type, target); + for(auto const & signature : signatures) { + fake_record.signature = signature; + fake_record.target_mass = detector_model->GetTargetMass(target); + // Add total cross section times density to the total prob + fake_prob = target_density * cross_section->TotalCrossSection(fake_record); + total_prob += fake_prob; + xsec_prob += fake_prob; + // Add total prob to probs + probs.push_back(total_prob); + // Add target and cross section pointer to the lists + matching_targets.push_back(target); + matching_cross_sections.push_back(cross_section); + matching_signatures.push_back(signature); } } } } - if (interactions->HasDecays()) { - for(auto const & decay : interactions->GetDecays() ) { - for(auto const & signature : decay->GetPossibleSignaturesFromParent(record.signature.primary_type)) { - fake_record.signature = signature; - // fake_prob has units of 1/cm to match cross section probabilities - fake_prob = 1./(decay->TotalDecayLengthForFinalState(fake_record)/siren::utilities::Constants::cm); - total_prob += fake_prob; - // Add total prob to probs - probs.push_back(total_prob); - // Add target and decay pointer to the lists - matching_targets.push_back(siren::dataclasses::ParticleType::Decay); - matching_decays.push_back(decay); - matching_signatures.push_back(signature); - } - } - } - //std::cout << "injector :: sample cross sections: after obtaining signatures" << std::endl; - if(total_prob == 0) - throw(siren::utilities::InjectionFailure("No valid interactions for this event!")); - // Throw a random number - double r = random->Uniform(0, total_prob); - // Choose the target and cross section - unsigned int index = 0; - for(; (index+1 < probs.size()) and (r > probs[index]); ++index) {} - record.signature.target_type = matching_targets[index]; - record.signature = matching_signatures[index]; - double selected_prob = 0.0; - for(unsigned int i=0; i 0 ? probs[i] - probs[i - 1] : probs[i]); + } + if (interactions->HasDecays()) { + for(auto const & decay : interactions->GetDecays() ) { + for(auto const & signature : decay->GetPossibleSignaturesFromParent(record.signature.primary_type)) { + fake_record.signature = signature; + // fake_prob has units of 1/cm to match cross section probabilities + fake_prob = 1./(decay->TotalDecayLengthForFinalState(fake_record)/siren::utilities::Constants::cm); + total_prob += fake_prob; + // Add total prob to probs + probs.push_back(total_prob); + // Add target and decay pointer to the lists + matching_targets.push_back(siren::dataclasses::ParticleType::Decay); + matching_decays.push_back(decay); + matching_signatures.push_back(signature); } } - if(selected_prob == 0) - throw(siren::utilities::InjectionFailure("No valid interactions for this event!")); - record.target_mass = detector_model->GetTargetMass(record.signature.target_type); - siren::dataclasses::CrossSectionDistributionRecord xsec_record(record); - if(r <= xsec_prob) { - //std::cout << "injector::sample cross section: going into sampel final state" << std::endl; - matching_cross_sections[index]->SampleFinalState(xsec_record, random); - //std::cout << "injector::sample cross section: finished sampling" << std::endl; - - } else { - matching_decays[index - matching_cross_sections.size()]->SampleFinalState(xsec_record, random); + } + //std::cout << "injector :: sample cross sections: after obtaining signatures" << std::endl; + if(total_prob == 0) + throw(siren::utilities::InjectionFailure("No valid interactions for this event!")); + // Throw a random number + double r = random->Uniform(0, total_prob); + // Choose the target and cross section + unsigned int index = 0; + for(; (index+1 < probs.size()) and (r > probs[index]); ++index) {} + record.signature.target_type = matching_targets[index]; + record.signature = matching_signatures[index]; + double selected_prob = 0.0; + for(unsigned int i=0; i 0 ? probs[i] - probs[i - 1] : probs[i]); } - ////std::cout << "injector::sample cross section: calling finalizing" << std::endl; - xsec_record.Finalize(record); - ////std::cout << "injector::sample cross section: finished finalizing" << std::endl; + } + if(selected_prob == 0) + throw(siren::utilities::InjectionFailure("No valid interactions for this event!")); + record.target_mass = detector_model->GetTargetMass(record.signature.target_type); + siren::dataclasses::CrossSectionDistributionRecord xsec_record(record); + if(r <= xsec_prob) { + //std::cout << "injector::sample cross section: going into sampel final state" << std::endl; + matching_cross_sections[index]->SampleFinalState(xsec_record, random); + //std::cout << "injector::sample cross section: finished sampling" << std::endl; + } else { + matching_decays[index - matching_cross_sections.size()]->SampleFinalState(xsec_record, random); } + ////std::cout << "injector::sample cross section: calling finalizing" << std::endl; + xsec_record.Finalize(record); + ////std::cout << "injector::sample cross section: finished finalizing" << std::endl; + } // Function to sample secondary processes diff --git a/projects/injection/private/Weighter.cxx b/projects/injection/private/Weighter.cxx index d393ec518..87f36ce9e 100644 --- a/projects/injection/private/Weighter.cxx +++ b/projects/injection/private/Weighter.cxx @@ -110,31 +110,14 @@ double Weighter::EventWeight(siren::dataclasses::InteractionTree const & tree) c double inv_weight = 0; for(unsigned int idx = 0; idx < injectors.size(); ++idx) { - // std::cout << "New Event" << std::endl; double physical_probability = 1.0; double generation_probability = injectors[idx]->EventsToInject();//GenerationProbability(tree); for(auto const & datum : tree.tree) { - // std::cout << "new depth " << datum->depth() << std::endl; - // skip weighting if record contains hadronization - if (datum->record.signature.target_type == siren::dataclasses::Particle::ParticleType::Hadronization) { - continue; - } std::tuple bounds; if(datum->depth() == 0) { bounds = injectors[idx]->PrimaryInjectionBounds(datum->record); physical_probability *= primary_process_weighters[idx]->PhysicalProbability(bounds, datum->record); generation_probability *= primary_process_weighters[idx]->GenerationProbability(*datum); - // for debugging purposes: nan weights are frequnetly detected - if (physical_probability == 0) { - std::cout << "zero physics depth 0: " << datum->record.signature.primary_type << std::endl; - } else if (std::isinf(physical_probability)) { - std::cout << "inf physics depth 0: " << datum->record.signature.primary_type << std::endl; - } - if (generation_probability == 0) { - std::cout << "zero gen depth 0: " << datum->record.signature.primary_type << std::endl; - } else if (std::isinf(generation_probability)) { - std::cout << "inf gen depth 0: " << datum->record.signature.primary_type << std::endl; - } } else { try { @@ -143,16 +126,6 @@ double Weighter::EventWeight(siren::dataclasses::InteractionTree const & tree) c double gen_prob = secondary_process_weighter_maps[idx].at(datum->record.signature.primary_type)->GenerationProbability(*datum); physical_probability *= phys_prob; generation_probability *= gen_prob; - // if (phys_prob == 0) { - // std::cout << "zero physics: " << datum->record.signature.primary_type << std::endl; - // } else if (std::isinf(phys_prob)) { - // std::cout << "inf physics: " << datum->record.signature.primary_type << std::endl; - // } - // if (gen_prob == 0) { - // std::cout << "zero gen: " << datum->record.signature.primary_type << std::endl; - // } else if (std::isinf(gen_prob)) { - // std::cout << "inf gen: " << datum->record.signature.primary_type << std::endl; - // } } catch(const std::out_of_range& oor) { std::cout << "Out of Range error: " << oor.what() << '\n'; return 0; @@ -161,18 +134,6 @@ double Weighter::EventWeight(siren::dataclasses::InteractionTree const & tree) c } inv_weight += generation_probability / physical_probability; - // if (physical_probability == 0) { - // std::cout << "Event has 0 physical probability, leading to: " << inv_weight << " " << 1./inv_weight << std::endl; - // } else if (physical_probability != physical_probability) { - // std::cout << "Event has inf physical probability, leading to: " << inv_weight << " " << 1./inv_weight << std::endl; - // } - // if (generation_probability == 0) { - // std::cout << "Event has 0 generation probability, leading to: " << inv_weight << " " << 1./inv_weight << std::endl; - // } else if (generation_probability != generation_probability) { - // std::cout << "Event has inf generation probability, leading to: " << inv_weight << " " << 1./inv_weight << std::endl; - // } - // std::cout << "gen and physics prob is " << generation_probability << " " << physical_probability << std::endl; - // std::cout << "inverse weight and final weight is " << inv_weight << " " << 1./inv_weight << std::endl; } return 1./inv_weight; } diff --git a/projects/injection/private/WeightingUtils.cxx b/projects/injection/private/WeightingUtils.cxx index 0b92b5cc7..2f9e27036 100644 --- a/projects/injection/private/WeightingUtils.cxx +++ b/projects/injection/private/WeightingUtils.cxx @@ -69,29 +69,15 @@ double CrossSectionProbability(std::shared_ptr signatures = cross_section->GetPossibleSignaturesFromParents(record.signature.primary_type, target); for(auto const & signature : signatures) { - // check here for 0 generation probability fake_record.signature = signature; fake_record.target_mass = detector_model->GetTargetMass(target); // Add total cross section times density to the total prob double total_xs = cross_section->TotalCrossSection(fake_record); double target_prob = target_density * total_xs; - // if (total_xs == 0) { - // std::cout << "total cross section give 0 for process of " << record.signature.primary_type << std::endl; - // std::cout << "for signature " << fake_record.signature << std::endl; - // } else if (std::isinf(total_xs)) { - // std::cout << "total cross section give inf for process of " << record.signature.primary_type << std::endl; - // std::cout << "for signature " << fake_record.signature << std::endl; - // } total_prob += target_prob; // Add up total cross section times density times final state prob for matching signatures if(signature == record.signature) { - // selected_prob += target_prob; double final_prob = cross_section->FinalStateProbability(record); - // if (final_prob == 0) { - // std::cout << "final state prob give 0 for process of " << record.signature.primary_type << std::endl; - // } else if (std::isinf(final_prob)) { - // std::cout << "final state prob give inf for process of " << record.signature.primary_type << std::endl; - // } selected_final_state += target_prob * final_prob; } } diff --git a/projects/injection/public/SIREN/injection/ColumnDepthLeptonInjector.h b/projects/injection/public/SIREN/injection/ColumnDepthLeptonInjector.h index 9caa13eb7..d81aaa17f 100644 --- a/projects/injection/public/SIREN/injection/ColumnDepthLeptonInjector.h +++ b/projects/injection/public/SIREN/injection/ColumnDepthLeptonInjector.h @@ -22,7 +22,6 @@ #include "SIREN/interactions/CrossSection.h" #include "SIREN/interactions/Decay.h" -#include "SIREN/interactions/Hadronization.h" #include "SIREN/detector/DetectorModel.h" #include "SIREN/distributions/primary/vertex/ColumnDepthPositionDistribution.h" diff --git a/projects/injection/public/SIREN/injection/CylinderVolumeLeptonInjector.h b/projects/injection/public/SIREN/injection/CylinderVolumeLeptonInjector.h index 1aabfd1f5..7f5ddc051 100644 --- a/projects/injection/public/SIREN/injection/CylinderVolumeLeptonInjector.h +++ b/projects/injection/public/SIREN/injection/CylinderVolumeLeptonInjector.h @@ -23,7 +23,6 @@ #include "SIREN/interactions/InteractionCollection.h" #include "SIREN/interactions/CrossSection.h" #include "SIREN/interactions/Decay.h" -#include "SIREN/interactions/Hadronization.h" #include "SIREN/detector/DetectorModel.h" #include "SIREN/distributions/primary/vertex/CylinderVolumePositionDistribution.h" diff --git a/projects/injection/public/SIREN/injection/DecayRangeLeptonInjector.h b/projects/injection/public/SIREN/injection/DecayRangeLeptonInjector.h index d2eb44ded..d5d7868f0 100644 --- a/projects/injection/public/SIREN/injection/DecayRangeLeptonInjector.h +++ b/projects/injection/public/SIREN/injection/DecayRangeLeptonInjector.h @@ -23,7 +23,6 @@ #include "SIREN/interactions/InteractionCollection.h" #include "SIREN/interactions/CrossSection.h" #include "SIREN/interactions/Decay.h" -#include "SIREN/interactions/Hadronization.h" #include "SIREN/detector/DetectorModel.h" #include "SIREN/distributions/primary/vertex/DecayRangeFunction.h" diff --git a/projects/injection/public/SIREN/injection/RangedLeptonInjector.h b/projects/injection/public/SIREN/injection/RangedLeptonInjector.h index a835b9c45..fa50a3285 100644 --- a/projects/injection/public/SIREN/injection/RangedLeptonInjector.h +++ b/projects/injection/public/SIREN/injection/RangedLeptonInjector.h @@ -21,7 +21,6 @@ #include "SIREN/interactions/InteractionCollection.h" #include "SIREN/interactions/CrossSection.h" #include "SIREN/interactions/Decay.h" -#include "SIREN/interactions/Hadronization.h" #include "SIREN/detector/DetectorModel.h" #include "SIREN/distributions/primary/vertex/RangeFunction.h" diff --git a/projects/injection/public/SIREN/injection/Weighter.tcc b/projects/injection/public/SIREN/injection/Weighter.tcc index 1e8022144..7ad973c78 100644 --- a/projects/injection/public/SIREN/injection/Weighter.tcc +++ b/projects/injection/public/SIREN/injection/Weighter.tcc @@ -121,9 +121,12 @@ double ProcessWeighter::InteractionProbability(std::tuple> const & xs_list = target_xs.second; double total_xs = 0.0; for(auto const & xs : xs_list) { - fake_record.signature.primary_type = record.signature.primary_type; - fake_record.signature.target_type = target_xs.first; - total_xs += xs->TotalCrossSectionAllFinalStates(fake_record); + std::vector signatures = xs->GetPossibleSignaturesFromParents(record.signature.primary_type, target_xs.first); + for(auto const & signature : signatures) { + fake_record.signature = signature; + // Add total cross section + total_xs += xs->TotalCrossSection(fake_record); + } } total_cross_sections.push_back(total_xs); } @@ -178,9 +181,12 @@ double ProcessWeighter::NormalizedPositionProbability(std::tuple> const & xs_list = target_xs.second; double total_xs = 0.0; for(auto const & xs : xs_list) { - fake_record.signature.primary_type = record.signature.primary_type; - fake_record.signature.target_type = target_xs.first; - total_xs += xs->TotalCrossSectionAllFinalStates(fake_record); + std::vector signatures = xs->GetPossibleSignaturesFromParents(record.signature.primary_type, target_xs.first); + for(auto const & signature : signatures) { + fake_record.signature = signature; + // Add total cross section + total_xs += xs->TotalCrossSection(fake_record); + } } total_cross_sections.push_back(total_xs); } @@ -207,28 +213,16 @@ double ProcessWeighter::PhysicalProbability(std::tupleGetInteractions(), record); - // if (prob == 0) { - // std::cout << "XSec probability is 0" << std::endl; - // } physical_probability *= prob; for(auto physical_dist : unique_phys_distributions) { physical_probability *= physical_dist->GenerationProbability(detector_model, phys_process->GetInteractions(), record); - // if (physical_dist->GenerationProbability(detector_model, phys_process->GetInteractions(), record) == 0) { - // std::cout << "physical dist Generation probablity is 0" << std::endl; - // } } return normalization * physical_probability; @@ -237,19 +231,10 @@ double ProcessWeighter::PhysicalProbability(std::tuple double ProcessWeighter::GenerationProbability(siren::dataclasses::InteractionTreeDatum const & datum ) const { double gen_probability = siren::injection::CrossSectionProbability(detector_model, inj_process->GetInteractions(), datum.record); - // if (gen_probability == 0) { - // std::cout << "Gen Cross section probability is 0" << std::endl; - // } + for(auto gen_dist : unique_gen_distributions) { - // if (gen_dist->GenerationProbability(detector_model, inj_process->GetInteractions(), datum.record) == 0) { - // std::cout << "generation dist gen probability is 0" << std::endl; - // } gen_probability *= gen_dist->GenerationProbability(detector_model, inj_process->GetInteractions(), datum.record); } - - // if (gen_probability == 0) { - // std::cout << "tcc file gen prob is 0" << std::endl; - // } return gen_probability; } diff --git a/projects/interactions/CMakeLists.txt b/projects/interactions/CMakeLists.txt index dd58d9c9f..67e9673af 100644 --- a/projects/interactions/CMakeLists.txt +++ b/projects/interactions/CMakeLists.txt @@ -19,7 +19,6 @@ LIST (APPEND interactions_SOURCES ${PROJECT_SOURCE_DIR}/projects/interactions/private/DummyCrossSection.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/pyDarkNewsCrossSection.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/pyDarkNewsDecay.cxx - ${PROJECT_SOURCE_DIR}/projects/interactions/private/Hadronization.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/CharmMesonDecay.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/CharmMesonDecay3Body.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/DMesonELoss.cxx diff --git a/projects/interactions/private/Hadronization.cxx b/projects/interactions/private/Hadronization.cxx deleted file mode 100644 index 7833c0735..000000000 --- a/projects/interactions/private/Hadronization.cxx +++ /dev/null @@ -1,17 +0,0 @@ -#include "SIREN/interactions/Hadronization.h" -#include "SIREN/dataclasses/InteractionRecord.h" - -namespace siren { -namespace interactions { - -Hadronization::Hadronization() {} - -bool Hadronization::operator==(Hadronization const & other) const { - if(this == &other) - return true; - else - return this->equal(other); -} - -} // namespace interactions -} // namespace siren \ No newline at end of file diff --git a/projects/interactions/private/InteractionCollection.cxx b/projects/interactions/private/InteractionCollection.cxx index 56327b961..015e3449c 100644 --- a/projects/interactions/private/InteractionCollection.cxx +++ b/projects/interactions/private/InteractionCollection.cxx @@ -10,7 +10,6 @@ #include "SIREN/interactions/Interaction.h" // for Interaction #include "SIREN/interactions/CrossSection.h" // for CrossSe... #include "SIREN/interactions/Decay.h" // for Decay -#include "SIREN/interactions/Hadronization.h" // for Decay #include "SIREN/dataclasses/InteractionRecord.h" // for Interac... #include "SIREN/dataclasses/InteractionSignature.h" // for Interac... #include "SIREN/dataclasses/Particle.h" // for Particle @@ -62,21 +61,9 @@ InteractionCollection::InteractionCollection(siren::dataclasses::ParticleType pr InitializeTargetTypes(); } -InteractionCollection::InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> hadronizations) : primary_type(primary_type), hadronizations(hadronizations) { - InitializeTargetTypes(); -} -InteractionCollection::InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> cross_sections, std::vector> hadronizations) : primary_type(primary_type), cross_sections(cross_sections), hadronizations(hadronizations) { - InitializeTargetTypes(); -} -InteractionCollection::InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> decays, std::vector> hadronizations) : primary_type(primary_type), decays(decays), hadronizations(hadronizations) { - InitializeTargetTypes(); -} -InteractionCollection::InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> cross_sections, std::vector> decays, std::vector> hadronizations) : primary_type(primary_type), cross_sections(cross_sections), decays(decays), hadronizations(hadronizations) { - InitializeTargetTypes(); -} InteractionCollection::InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> interactions) : primary_type(primary_type) { for(auto interaction : interactions) { diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 47d2659db..4b5ca7206 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -239,10 +239,6 @@ double QuarkDISFromSpline::getHadronMass(siren::dataclasses::ParticleType hadron return 1.96834; // GeV (PDG 2022); no Constants::DsMass yet case siren::dataclasses::ParticleType::DsMinus: return 1.96834; - case siren::dataclasses::ParticleType::Charm: - return( siren::utilities::Constants::CharmMass); - case siren::dataclasses::ParticleType::CharmBar: - return( siren::utilities::Constants::CharmMass); default: return(0.0); } diff --git a/projects/interactions/private/pybindings/Hadronization.h b/projects/interactions/private/pybindings/Hadronization.h deleted file mode 100644 index 513836cfe..000000000 --- a/projects/interactions/private/pybindings/Hadronization.h +++ /dev/null @@ -1,34 +0,0 @@ -#include -#include -#include - -#include -#include -#include - -#include "../../public/SIREN/interactions/Hadronization.h" - -#include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" -#include "../../../dataclasses/public/SIREN/dataclasses/InteractionRecord.h" -#include "../../../dataclasses/public/SIREN/dataclasses/InteractionSignature.h" -#include "../../../geometry/public/SIREN/geometry/Geometry.h" -#include "../../../utilities/public/SIREN/utilities/Random.h" - -using namespace pybind11; -using namespace siren::interactions; - - -void register_Hadronization(pybind11::module_ & m) { - using namespace pybind11; - using namespace siren::interactions; - - class_>(m, "Hadronization") - // .def(init<>()) - .def("__eq__", [](const Hadronization &self, const Hadronization &other){ return self == other; }) - .def("equal", &Hadronization::equal) - .def("SampleFinalState", &Hadronization::SampleFinalState) - .def("GetPossibleSignatures", &Hadronization::GetPossibleSignatures) - .def("GetPossibleSignaturesFromParent", &Hadronization::GetPossibleSignaturesFromParent) - .def("FragmentationFraction", &Hadronization::FragmentationFraction) - ; -} diff --git a/projects/interactions/private/pybindings/InteractionCollection.h b/projects/interactions/private/pybindings/InteractionCollection.h index 711dee249..3262a2600 100644 --- a/projects/interactions/private/pybindings/InteractionCollection.h +++ b/projects/interactions/private/pybindings/InteractionCollection.h @@ -10,7 +10,6 @@ #include "../../public/SIREN/interactions/CrossSection.h" #include "../../public/SIREN/interactions/InteractionCollection.h" #include "../../public/SIREN/interactions/Decay.h" -#include "../../public/SIREN/interactions/Hadronization.h" #include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" #include "../../../geometry/public/SIREN/geometry/Geometry.h" @@ -24,11 +23,7 @@ void register_InteractionCollection(pybind11::module_ & m) { .def(init<>()) .def(init>>()) .def(init>>()) - .def(init>>()) .def(init>, std::vector>>()) - .def(init>, std::vector>>()) - .def(init>, std::vector>>()) - .def(init>, std::vector>, std::vector>>()) .def(init>>()) .def(self == self) .def("GetPrimaryType",&InteractionCollection::GetPrimaryType) @@ -36,7 +31,6 @@ void register_InteractionCollection(pybind11::module_ & m) { .def("GetDecays",&InteractionCollection::GetDecays, return_value_policy::reference_internal) .def("HasCrossSections",&InteractionCollection::HasCrossSections) .def("HasDecays",&InteractionCollection::HasDecays) - .def("HasHadronizations",&InteractionCollection::HasHadronizations) .def("GetCrossSectionsForTarget",&InteractionCollection::GetCrossSectionsForTarget, return_value_policy::reference_internal) .def("GetCrossSectionsByTarget",&InteractionCollection::GetCrossSectionsByTarget, return_value_policy::reference_internal) .def("TotalCrossSectionByTarget",&InteractionCollection::TotalCrossSectionByTarget) diff --git a/projects/interactions/private/pybindings/interactions.cxx b/projects/interactions/private/pybindings/interactions.cxx index 64ecf72f2..72f3b880b 100644 --- a/projects/interactions/private/pybindings/interactions.cxx +++ b/projects/interactions/private/pybindings/interactions.cxx @@ -17,7 +17,6 @@ #include "../../public/SIREN/interactions/HNLDecay.h" #include "../../public/SIREN/interactions/InteractionCollection.h" #include "../../public/SIREN/interactions/QuarkDISFromSpline.h" -#include "../../public/SIREN/interactions/Hadronization.h" #include "../../public/SIREN/interactions/CharmMesonDecay.h" #include "../../public/SIREN/interactions/CharmMesonDecay3Body.h" #include "../../public/SIREN/interactions/DMesonELoss.h" @@ -40,7 +39,6 @@ #include "./HNLDISFromSpline.h" #include "./HNLDecay.h" #include "./InteractionCollection.h" -#include "./Hadronization.h" #include "./CharmMesonDecay.h" #include "./CharmMesonDecay3Body.h" #include "./DMesonELoss.h" @@ -74,7 +72,6 @@ PYBIND11_MODULE(interactions,m) { register_HNLDecay(m); register_InteractionCollection(m); register_QuarkDISFromSpline(m); - register_Hadronization(m); register_CharmMesonDecay(m); register_CharmMesonDecay3Body(m); register_DMesonELoss(m); diff --git a/projects/interactions/public/SIREN/interactions/Hadronization.h b/projects/interactions/public/SIREN/interactions/Hadronization.h deleted file mode 100644 index 6f9312769..000000000 --- a/projects/interactions/public/SIREN/interactions/Hadronization.h +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once -#ifndef SIREN_Hadronization_H -#define SIREN_Hadronization_H - -#include // for shared_ptr -#include // for string -#include // for vector -#include // for uint32_t - -#include -#include -#include -#include -#include -#include - -#include "SIREN/dataclasses/Particle.h" // for Particle -#include "SIREN/dataclasses/InteractionSignature.h" // for InteractionSignature -#include "SIREN/dataclasses/InteractionRecord.h" // for InteractionSignature - -#include "SIREN/utilities/Random.h" // for SIREN_random -#include "SIREN/geometry/Geometry.h" - -namespace siren { namespace dataclasses { class InteractionRecord; } } -namespace siren { namespace dataclasses { struct InteractionSignature; } } -namespace siren { namespace utilities { class SIREN_random; } } - -namespace siren { -namespace interactions { - -class Hadronization { -friend cereal::access; -private: -public: - Hadronization(); - virtual ~Hadronization() {}; - bool operator==(Hadronization const & other) const; - virtual bool equal(Hadronization const & other) const = 0; - - virtual void SampleFinalState(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr) const = 0; - virtual std::vector GetPossibleSignatures() const = 0; - virtual std::vector GetPossibleSignaturesFromParent(siren::dataclasses::Particle::ParticleType primary) const = 0; - virtual double FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const = 0; - - template - void save(Archive & archive, std::uint32_t const version) const {}; - template - void load(Archive & archive, std::uint32_t const version) {}; - -}; // class Hadronization - -} // namespace interactions -} // namespace siren - -CEREAL_CLASS_VERSION(siren::interactions::Hadronization, 0); - - -#endif // SIREN_Hadronization_H diff --git a/projects/interactions/public/SIREN/interactions/InteractionCollection.h b/projects/interactions/public/SIREN/interactions/InteractionCollection.h index 9f756590c..2d2cb2d50 100644 --- a/projects/interactions/public/SIREN/interactions/InteractionCollection.h +++ b/projects/interactions/public/SIREN/interactions/InteractionCollection.h @@ -27,7 +27,6 @@ #include "SIREN/dataclasses/Particle.h" // for Particle #include "SIREN/interactions/CrossSection.h" #include "SIREN/interactions/Decay.h" -#include "SIREN/interactions/Hadronization.h" namespace siren { namespace dataclasses { class InteractionRecord; } } @@ -40,7 +39,6 @@ class InteractionCollection { siren::dataclasses::ParticleType primary_type; std::vector> cross_sections; std::vector> decays; - std::vector> hadronizations; std::map>> cross_sections_by_target; std::set target_types; @@ -51,21 +49,15 @@ class InteractionCollection { virtual ~InteractionCollection() {}; InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> cross_sections); InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> decays); - InteractionCollection(siren::dataclasses::Particle::ParticleType primary_type, std::vector> hadronizations); - InteractionCollection(siren::dataclasses::Particle::ParticleType primary_type, std::vector> decays, std::vector> hadronizations); - InteractionCollection(siren::dataclasses::Particle::ParticleType primary_type, std::vector> cross_sections, std::vector> hadronizations); - InteractionCollection(siren::dataclasses::Particle::ParticleType primary_type, std::vector> cross_sections, std::vector> decays, std::vector> hadronizations); InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> cross_sections, std::vector> decays); InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> interactions); bool operator==(InteractionCollection const & other) const; std::vector> const & GetCrossSections() const {return cross_sections;} std::vector> const & GetDecays() const {return decays;} - std::vector> const & GetHadronizations() const {return hadronizations;} bool const HasCrossSections() const {return cross_sections.size() > 0;} bool const HasDecays() const {return decays.size() > 0;} - bool const HasHadronizations() const {return hadronizations.size() > 0;} std::vector> const & GetCrossSectionsForTarget(siren::dataclasses::ParticleType p) const; std::map>> const & GetCrossSectionsByTarget() const { @@ -89,7 +81,6 @@ class InteractionCollection { archive(cereal::make_nvp("TargetTypes", target_types)); archive(cereal::make_nvp("CrossSections", cross_sections)); archive(cereal::make_nvp("Decays", decays)); - archive(cereal::make_nvp("Hadronizations", hadronizations)); } else { throw std::runtime_error("InteractionCollection only supports version <= 0!"); } @@ -102,7 +93,6 @@ class InteractionCollection { archive(cereal::make_nvp("TargetTypes", target_types)); archive(cereal::make_nvp("CrossSections", cross_sections)); archive(cereal::make_nvp("Decays", decays)); - archive(cereal::make_nvp("Hadronizations", hadronizations)); InitializeTargetTypes(); } else { throw std::runtime_error("InteractionCollection only supports version <= 0!"); From d7764ae9ee656c8e60a6666caed5d5b2b5a99433 Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Wed, 20 May 2026 23:16:37 -0400 Subject: [PATCH 51/93] more PR#74 edits, especially vendor updates and pythia cmake gating --- .gitignore | 3 - CMakeLists.txt | 6 + cmake/Packages/PYTHIA8.cmake | 146 ++++++++++++++++++ projects/interactions/CMakeLists.txt | 28 ++-- .../pybindings/PythiaDISCrossSection.h | 12 +- .../private/pybindings/interactions.cxx | 2 + vendor/cereal | 2 +- vendor/photospline | 2 +- 8 files changed, 181 insertions(+), 20 deletions(-) create mode 100644 cmake/Packages/PYTHIA8.cmake diff --git a/.gitignore b/.gitignore index 03354431a..c57fe85da 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,3 @@ __pycache__/ # output dirs output/ - -# vendors -vendor/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d5d59faa..9baa9c230 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,8 @@ option(SIREN_PYTHON_PACKAGE ) option(SIREN_WITH_MARLEY "Enable MARLEY support if available" ON) option(SIREN_REQUIRE_MARLEY "Fail configure if MARLEY requested but not found" OFF) +option(SIREN_WITH_PYTHIA8 "Enable Pythia8 support if available" OFF) +option(SIREN_REQUIRE_PYTHIA8 "Fail configure if Pythia8 requested but not found" OFF) # include cmake dependencies include(GNUInstallDirs) @@ -160,6 +162,10 @@ include(MARLEY) if(TARGET MARLEY) apply_siren_compile_options(MARLEY) endif() +include(PYTHIA8) +if(TARGET PYTHIA8) + apply_siren_compile_options(PYTHIA8) +endif() # load macros for googletest include(testing) diff --git a/cmake/Packages/PYTHIA8.cmake b/cmake/Packages/PYTHIA8.cmake new file mode 100644 index 000000000..5996dfc61 --- /dev/null +++ b/cmake/Packages/PYTHIA8.cmake @@ -0,0 +1,146 @@ +# cmake/Packages/PYTHIA8.cmake +# +# Optional Pythia8 (+ LHAPDF) hookup, modeled on MARLEY.cmake. +# +# Usage (top-level CMakeLists.txt): +# option(SIREN_WITH_PYTHIA8 "Enable Pythia8 support if available" OFF) +# option(SIREN_REQUIRE_PYTHIA8 "Fail configure if Pythia8 requested but not found" OFF) +# include(cmake/Packages/PYTHIA8.cmake) +# if(PYTHIA8_FOUND) +# target_link_libraries( PUBLIC PYTHIA8) +# target_compile_definitions( PUBLIC SIREN_HAS_PYTHIA8=1) +# endif() + +set(PYTHIA8_FOUND FALSE) + +if(DEFINED SIREN_WITH_PYTHIA8 AND NOT SIREN_WITH_PYTHIA8) + message(STATUS "Pythia8 support disabled (SIREN_WITH_PYTHIA8=OFF)") + return() +endif() + +set(_pythia8_ok TRUE) + +# ----------------------------- +# 1) Find Pythia8 installation prefix +# ----------------------------- +unset(PYTHIA8_PREFIX CACHE) +unset(PYTHIA8_PREFIX) + +if(DEFINED ENV{PYTHIA8_ROOT} AND NOT "$ENV{PYTHIA8_ROOT}" STREQUAL "") + set(PYTHIA8_PREFIX "$ENV{PYTHIA8_ROOT}") + message(STATUS "Using PYTHIA8_ROOT from environment: ${PYTHIA8_PREFIX}") +elseif(DEFINED PYTHIA8_DIR AND NOT "${PYTHIA8_DIR}" STREQUAL "") + set(PYTHIA8_PREFIX "${PYTHIA8_DIR}") + message(STATUS "Using PYTHIA8_DIR from cache/cmdline: ${PYTHIA8_PREFIX}") +else() + find_path(PYTHIA8_PREFIX + NAMES include/Pythia8/Pythia.h + PATHS ${CMAKE_PREFIX_PATH} /usr /usr/local /opt/local /opt/homebrew + DOC "Root directory for Pythia8 installation" + ) +endif() + +if(NOT PYTHIA8_PREFIX) + set(_pythia8_ok FALSE) + message(STATUS "Pythia8 not found (no PYTHIA8_ROOT/PYTHIA8_DIR and not in CMAKE_PREFIX_PATH).") +endif() + +# ----------------------------- +# 2) Headers +# ----------------------------- +if(_pythia8_ok) + set(PYTHIA8_INCLUDE_DIR "${PYTHIA8_PREFIX}/include") + if(NOT EXISTS "${PYTHIA8_INCLUDE_DIR}/Pythia8/Pythia.h") + set(_pythia8_ok FALSE) + message(STATUS "Pythia8 headers not found at: ${PYTHIA8_INCLUDE_DIR}/Pythia8/Pythia.h") + endif() +endif() + +# ----------------------------- +# 3) Library +# ----------------------------- +if(_pythia8_ok) + find_library(PYTHIA8_LIBRARY + NAMES pythia8 Pythia8 + PATHS "${PYTHIA8_PREFIX}/lib64" "${PYTHIA8_PREFIX}/lib" + NO_DEFAULT_PATH + DOC "Pythia8 library" + ) + + if(NOT PYTHIA8_LIBRARY) + set(_pythia8_ok FALSE) + message(STATUS "Pythia8 library not found under ${PYTHIA8_PREFIX}/lib{,64}.") + endif() +endif() + +# ----------------------------- +# 4) Optional LHAPDF (PythiaDISCrossSection uses LHAPDF6 PDF set) +# ----------------------------- +set(_lhapdf_ok TRUE) +unset(LHAPDF_PREFIX CACHE) +unset(LHAPDF_PREFIX) + +if(_pythia8_ok) + if(DEFINED ENV{LHAPDF_ROOT} AND NOT "$ENV{LHAPDF_ROOT}" STREQUAL "") + set(LHAPDF_PREFIX "$ENV{LHAPDF_ROOT}") + message(STATUS "Using LHAPDF_ROOT from environment: ${LHAPDF_PREFIX}") + elseif(DEFINED LHAPDF_DIR AND NOT "${LHAPDF_DIR}" STREQUAL "") + set(LHAPDF_PREFIX "${LHAPDF_DIR}") + message(STATUS "Using LHAPDF_DIR from cache/cmdline: ${LHAPDF_PREFIX}") + else() + find_path(LHAPDF_PREFIX + NAMES include/LHAPDF/LHAPDF.h + PATHS ${CMAKE_PREFIX_PATH} /usr /usr/local /opt/local /opt/homebrew + DOC "Root directory for LHAPDF installation" + ) + endif() + + if(NOT LHAPDF_PREFIX) + set(_lhapdf_ok FALSE) + message(STATUS "LHAPDF not found (no LHAPDF_ROOT/LHAPDF_DIR and not in CMAKE_PREFIX_PATH); required for Pythia8 DIS support.") + else() + set(LHAPDF_INCLUDE_DIR "${LHAPDF_PREFIX}/include") + find_library(LHAPDF_LIBRARY + NAMES LHAPDF + PATHS "${LHAPDF_PREFIX}/lib64" "${LHAPDF_PREFIX}/lib" + NO_DEFAULT_PATH + DOC "LHAPDF library" + ) + if(NOT LHAPDF_LIBRARY) + set(_lhapdf_ok FALSE) + message(STATUS "LHAPDF library not found under ${LHAPDF_PREFIX}/lib{,64}.") + endif() + endif() + + if(NOT _lhapdf_ok) + set(_pythia8_ok FALSE) + endif() +endif() + +# ----------------------------- +# 5) Create imported target "PYTHIA8" (carries Pythia8 + LHAPDF link/include) +# ----------------------------- +if(_pythia8_ok) + if(NOT TARGET PYTHIA8) + add_library(PYTHIA8 UNKNOWN IMPORTED) + message(STATUS "Created imported target: PYTHIA8") + endif() + + set_target_properties(PYTHIA8 PROPERTIES + IMPORTED_LOCATION "${PYTHIA8_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${PYTHIA8_INCLUDE_DIR};${LHAPDF_INCLUDE_DIR}" + INTERFACE_LINK_LIBRARIES "${LHAPDF_LIBRARY};dl;pthread" + ) + + set(PYTHIA8_FOUND TRUE) + message(STATUS "Pythia8 enabled: prefix=${PYTHIA8_PREFIX} (LHAPDF prefix=${LHAPDF_PREFIX})") +endif() + +# ----------------------------- +# 6) Optional hard requirement behavior +# ----------------------------- +if(NOT PYTHIA8_FOUND) + if(DEFINED SIREN_REQUIRE_PYTHIA8 AND SIREN_REQUIRE_PYTHIA8) + message(FATAL_ERROR "SIREN_REQUIRE_PYTHIA8=ON but Pythia8 could not be found.") + endif() +endif() diff --git a/projects/interactions/CMakeLists.txt b/projects/interactions/CMakeLists.txt index 67e9673af..3efda22c1 100644 --- a/projects/interactions/CMakeLists.txt +++ b/projects/interactions/CMakeLists.txt @@ -23,7 +23,6 @@ LIST (APPEND interactions_SOURCES ${PROJECT_SOURCE_DIR}/projects/interactions/private/CharmMesonDecay3Body.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/DMesonELoss.cxx ${PROJECT_SOURCE_DIR}/projects/interactions/private/QuarkDISFromSpline.cxx - ${PROJECT_SOURCE_DIR}/projects/interactions/private/PythiaDISCrossSection.cxx ) if(TARGET MARLEY) @@ -31,22 +30,18 @@ if(TARGET MARLEY) ${PROJECT_SOURCE_DIR}/projects/interactions/private/MarleyCrossSection.cxx ) endif() +if(TARGET PYTHIA8) + LIST (APPEND interactions_SOURCES + ${PROJECT_SOURCE_DIR}/projects/interactions/private/PythiaDISCrossSection.cxx + ) +endif() add_library(SIREN_interactions OBJECT ${interactions_SOURCES}) set_property(TARGET SIREN_interactions PROPERTY POSITION_INDEPENDENT_CODE ON) -# Pythia8 + LHAPDF for PythiaDISCrossSection -# Defaults target our cluster installs; override via -DPYTHIA8_DIR=... on cmake cmdline -set(PYTHIA8_DIR "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/miaochenjin/local/pythia8315" - CACHE PATH "Pythia8 installation root") -set(LHAPDF_DIR "/cvmfs/icecube.opensciencegrid.org/py3-v4.1.1/RHEL_8_x86_64" - CACHE PATH "LHAPDF installation root") - target_include_directories(SIREN_interactions PUBLIC $ $ ${PYTHON_INCLUDE_DIRS} - ${PYTHIA8_DIR}/include - ${LHAPDF_DIR}/include ) find_package(GSL REQUIRED) target_link_libraries(SIREN_interactions @@ -67,8 +62,10 @@ target_link_libraries(SIREN_interactions ) apply_siren_compile_options(SIREN_interactions) -target_link_directories(SIREN_interactions PUBLIC ${PYTHIA8_DIR}/lib ${LHAPDF_DIR}/lib) -target_link_libraries(SIREN_interactions PUBLIC pythia8 LHAPDF dl pthread) +if(TARGET PYTHIA8) + target_link_libraries(SIREN_interactions PUBLIC PYTHIA8) + target_compile_definitions(SIREN_interactions PUBLIC SIREN_HAS_PYTHIA8) +endif() install(DIRECTORY "${PROJECT_SOURCE_DIR}/projects/interactions/public/" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} @@ -84,8 +81,11 @@ package_add_test(UnitTest_HNLDecay ${PROJECT_SOURCE_DIR}/projects/interactions/p #package_add_test(UnitTest_ElasticScattering ${PROJECT_SOURCE_DIR}/projects/interactions/private/test/ElasticScattering_TEST.cxx) pybind11_add_module(interactions ${PROJECT_SOURCE_DIR}/projects/interactions/private/pybindings/interactions.cxx) -target_link_directories(interactions PRIVATE ${PYTHIA8_DIR}/lib ${LHAPDF_DIR}/lib) -target_link_libraries(interactions PRIVATE SIREN photospline rk_static crundec_static pybind11::embed GSL::gsl GSL::gslcblas pythia8 LHAPDF dl pthread) +target_link_libraries(interactions PRIVATE SIREN photospline rk_static crundec_static pybind11::embed GSL::gsl GSL::gslcblas) +if(TARGET PYTHIA8) + target_link_libraries(interactions PRIVATE PYTHIA8) + target_compile_definitions(interactions PRIVATE SIREN_HAS_PYTHIA8) +endif() if(TARGET MARLEY) target_compile_definitions(interactions PRIVATE SIREN_HAS_MARLEY) endif() diff --git a/projects/interactions/private/pybindings/PythiaDISCrossSection.h b/projects/interactions/private/pybindings/PythiaDISCrossSection.h index 44a79e3fe..f6ab7c47f 100644 --- a/projects/interactions/private/pybindings/PythiaDISCrossSection.h +++ b/projects/interactions/private/pybindings/PythiaDISCrossSection.h @@ -7,13 +7,15 @@ #include #include "../../public/SIREN/interactions/CrossSection.h" -#include "../../public/SIREN/interactions/PythiaDISCrossSection.h" #include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" #include "../../../dataclasses/public/SIREN/dataclasses/InteractionRecord.h" #include "../../../dataclasses/public/SIREN/dataclasses/InteractionSignature.h" #include "../../../geometry/public/SIREN/geometry/Geometry.h" #include "../../../utilities/public/SIREN/utilities/Random.h" +#ifdef SIREN_HAS_PYTHIA8 +#include "../../public/SIREN/interactions/PythiaDISCrossSection.h" + void register_PythiaDISCrossSection(pybind11::module_ & m) { using namespace pybind11; using namespace siren::interactions; @@ -68,3 +70,11 @@ void register_PythiaDISCrossSection(pybind11::module_ & m) { .def("GetTargetMass",&PythiaDISCrossSection::GetTargetMass) .def("GetInteractionType",&PythiaDISCrossSection::GetInteractionType); } + +#else + +void register_PythiaDISCrossSection(pybind11::module_ & m) { + // Pythia8 is not available, so we do not register the PythiaDISCrossSection class. +} + +#endif diff --git a/projects/interactions/private/pybindings/interactions.cxx b/projects/interactions/private/pybindings/interactions.cxx index 72f3b880b..703ab4871 100644 --- a/projects/interactions/private/pybindings/interactions.cxx +++ b/projects/interactions/private/pybindings/interactions.cxx @@ -20,7 +20,9 @@ #include "../../public/SIREN/interactions/CharmMesonDecay.h" #include "../../public/SIREN/interactions/CharmMesonDecay3Body.h" #include "../../public/SIREN/interactions/DMesonELoss.h" +#ifdef SIREN_HAS_PYTHIA8 #include "../../public/SIREN/interactions/PythiaDISCrossSection.h" +#endif #include "./Interaction.h" diff --git a/vendor/cereal b/vendor/cereal index 02eace19a..d1fcec807 160000 --- a/vendor/cereal +++ b/vendor/cereal @@ -1 +1 @@ -Subproject commit 02eace19a99ce3cd564ca4e379753d69af08c2c8 +Subproject commit d1fcec807b372f04e4c1041b3058e11c12853e6e diff --git a/vendor/photospline b/vendor/photospline index bb68658a8..c6fb3ea9a 160000 --- a/vendor/photospline +++ b/vendor/photospline @@ -1 +1 @@ -Subproject commit bb68658a8776b9dabba8c3d3332b4294474d20c3 +Subproject commit c6fb3ea9a98f93705bc07b288b7f9264e621ac18 From b301354b6bfc21e59565f584906a0003c73b7a30 Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Thu, 21 May 2026 01:04:41 -0400 Subject: [PATCH 52/93] fix a controller error, seems to be introduced by an upstream PR on controller without changing constructor (PR125), please double check --- .gitignore | Bin 559 -> 577 bytes projects/interactions/CMakeLists.txt | Bin 4845 -> 5171 bytes python/SIREN_Controller.py | 50 ++++++++++++++++++++++++--- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index c57fe85dae138a96b7d0b46193dc1a64b4042c93..cd598ae0ddb0ba10844a852b098e35535b4ac510 100644 GIT binary patch delta 26 ScmZ3_a*$<%K9djw1^@s#F#>D= delta 7 OcmX@evYuswJ`(^7Q36^3 diff --git a/projects/interactions/CMakeLists.txt b/projects/interactions/CMakeLists.txt index 3efda22c1e857edaee0026fd9323a1fe035a7fb4..e5ba36a12b207c9fc5e9cddafd558178845a3f27 100644 GIT binary patch delta 336 ZcmaE>x>;kxTcLV}fdvC?zX!Fv4*<{s1mgez delta 7 Ocmdn2@m6)iTOj}s4FjD3 diff --git a/python/SIREN_Controller.py b/python/SIREN_Controller.py index 63c38ed6b..2c9b7c4c1 100644 --- a/python/SIREN_Controller.py +++ b/python/SIREN_Controller.py @@ -64,9 +64,32 @@ def __init__(self, events_to_inject, experiment=None, detector_model_file=None, # Empty list for our interaction trees self.events = [] - # Load the detector via upstream load_detector (handles both - # materials.dat and densities.dat in the named detector folder). - self.detector_model = _util.load_detector(experiment) + # Resolve detector model: either named experiment OR explicit file paths. + # PR #74 follow-up: previously this called _util.load_detector(experiment) + # unconditionally, which raised TypeError(model_regex.match(None)) when + # the caller intended to use explicit file overrides. + self._densities_file = None # used as fallback for fiducial volume + if detector_model_file is not None and materials_model_file is not None: + # Explicit-path branch: build DetectorModel directly from files + # (mirrors _util._detector_file_loader; do not call load_detector, + # which requires a named experiment). + self.detector_model = _detector.DetectorModel() + self.detector_model.LoadMaterialModel(materials_model_file) + self.detector_model.LoadDetectorModel(detector_model_file) + self._densities_file = detector_model_file + elif detector_model_file is None and materials_model_file is None: + if experiment is None: + raise ValueError( + "Must provide either `experiment` (named detector) or " + "both `detector_model_file` and `materials_model_file`." + ) + # Named-experiment branch: defer to upstream load_detector + self.detector_model = _util.load_detector(experiment) + else: + raise ValueError( + "Must provide both `detector_model_file` and `materials_model_file`, " + "or neither (and supply `experiment` instead)." + ) # Define the primary injection and physical process self.primary_injection_process = _injection.PrimaryInjectionProcess() @@ -338,7 +361,26 @@ def GetFiducialVolume(self): """ :return: identified fiducial volume for the experiment, None if not found """ - return _util.get_fiducial_volume(self.experiment) + if self.experiment is not None: + return _util.get_fiducial_volume(self.experiment) + # Explicit-path branch: parse fiducial directly from the densities file + # (mirrors _util.get_fiducial_volume; avoids get_detector_model_path(None)). + if self._densities_file is None: + return None + with open(self._densities_file) as f: + fiducial_line = None + detector_line = None + for line in f: + data = line.split() + if len(data) <= 0: + continue + if data[0] == "fiducial": + fiducial_line = line + elif data[0] == "detector": + detector_line = line + if fiducial_line is None or detector_line is None: + return None + return _detector.DetectorModel.ParseFiducialVolume(fiducial_line, detector_line) def GetVolumePositionDistributionFromSector(self, sector_name): geo = self.GetDetectorSectorGeometry(sector_name) From 08d410ec3036d14b04757447de6bb62a7b513cdc Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Thu, 21 May 2026 02:13:54 -0400 Subject: [PATCH 53/93] retracted some erroneous commits that accidentally appended NUL to the end of text files --- .gitignore | Bin 577 -> 559 bytes projects/interactions/CMakeLists.txt | Bin 5171 -> 4845 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/.gitignore b/.gitignore index cd598ae0ddb0ba10844a852b098e35535b4ac510..c57fe85dae138a96b7d0b46193dc1a64b4042c93 100644 GIT binary patch delta 7 OcmX@evYuswJ`(^7Q36^3 delta 26 ScmZ3_a*$<%K9djw1^@s#F#>D= diff --git a/projects/interactions/CMakeLists.txt b/projects/interactions/CMakeLists.txt index e5ba36a12b207c9fc5e9cddafd558178845a3f27..3efda22c1e857edaee0026fd9323a1fe035a7fb4 100644 GIT binary patch delta 7 Ocmdn2@m6)iTOj}s4FjD3 delta 336 ZcmaE>x>;kxTcLV}fdvC?zX!Fv4*<{s1mgez From b95d84e5fc8de0a8e934ffa37ece23ea67479593 Mon Sep 17 00:00:00 2001 From: Nicholas Kamp Date: Thu, 21 May 2026 13:43:48 -0400 Subject: [PATCH 54/93] remove whitespace changes --- projects/dataclasses/private/InteractionRecord.cxx | 4 ---- .../vertex/SecondaryBoundedVertexDistribution.cxx | 1 - .../vertex/SecondaryPhysicalVertexDistribution.cxx | 2 +- .../public/SIREN/injection/ColumnDepthLeptonInjector.h | 1 - .../public/SIREN/injection/CylinderVolumeLeptonInjector.h | 1 - .../public/SIREN/injection/DecayRangeLeptonInjector.h | 1 - .../public/SIREN/injection/RangedLeptonInjector.h | 1 - projects/interactions/private/InteractionCollection.cxx | 6 +----- .../private/pybindings/InteractionCollection.h | 1 - .../public/SIREN/interactions/InteractionCollection.h | 7 +------ 10 files changed, 3 insertions(+), 22 deletions(-) diff --git a/projects/dataclasses/private/InteractionRecord.cxx b/projects/dataclasses/private/InteractionRecord.cxx index 2a379316f..1dc247264 100644 --- a/projects/dataclasses/private/InteractionRecord.cxx +++ b/projects/dataclasses/private/InteractionRecord.cxx @@ -625,7 +625,6 @@ void PrimaryDistributionRecord::FinalizeAvailable(InteractionRecord & record) co } void PrimaryDistributionRecord::Finalize(InteractionRecord & record) const { - record.signature.primary_type = type; record.primary_id = GetID(); record.interaction_vertex = GetInteractionVertex(); @@ -855,13 +854,11 @@ void SecondaryParticleRecord::UpdateMomentum() const { } void SecondaryParticleRecord::Finalize(InteractionRecord & record) const { - assert(record.signature.secondary_types.at(secondary_index) == type); record.secondary_ids.at(secondary_index) = GetID(); record.secondary_masses.at(secondary_index) = GetMass(); record.secondary_momenta.at(secondary_index) = GetFourMomentum(); record.secondary_helicities.at(secondary_index) = GetHelicity(); - } ///////////////////////////////////////// @@ -980,7 +977,6 @@ SecondaryParticleRecord const & CrossSectionDistributionRecord::GetSecondaryPart } void CrossSectionDistributionRecord::Finalize(InteractionRecord & record) const { - record.target_id = target_id; record.target_mass = target_mass; record.target_helicity = target_helicity; diff --git a/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx b/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx index 86e438692..2a648aac5 100644 --- a/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx +++ b/projects/distributions/private/secondary/vertex/SecondaryBoundedVertexDistribution.cxx @@ -175,7 +175,6 @@ double SecondaryBoundedVertexDistribution::GenerationProbability(std::shared_ptr prob_density = interaction_density * exp(-log_one_minus_exp_of_negative(total_interaction_depth) - traversed_interaction_depth); } - return prob_density; } diff --git a/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx b/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx index b25455125..9b58ce9e7 100644 --- a/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx +++ b/projects/distributions/private/secondary/vertex/SecondaryPhysicalVertexDistribution.cxx @@ -55,6 +55,7 @@ void SecondaryPhysicalVertexDistribution::SampleVertex(std::shared_ptr::infinity()); path.ClipToOuterBounds(); @@ -136,7 +137,6 @@ double SecondaryPhysicalVertexDistribution::GenerationProbability(std::shared_pt prob_density = interaction_density * exp(-log_one_minus_exp_of_negative(total_interaction_depth) - traversed_interaction_depth); } - return prob_density; } diff --git a/projects/injection/public/SIREN/injection/ColumnDepthLeptonInjector.h b/projects/injection/public/SIREN/injection/ColumnDepthLeptonInjector.h index d81aaa17f..73d541132 100644 --- a/projects/injection/public/SIREN/injection/ColumnDepthLeptonInjector.h +++ b/projects/injection/public/SIREN/injection/ColumnDepthLeptonInjector.h @@ -22,7 +22,6 @@ #include "SIREN/interactions/CrossSection.h" #include "SIREN/interactions/Decay.h" - #include "SIREN/detector/DetectorModel.h" #include "SIREN/distributions/primary/vertex/ColumnDepthPositionDistribution.h" #include "SIREN/distributions/primary/vertex/DepthFunction.h" diff --git a/projects/injection/public/SIREN/injection/CylinderVolumeLeptonInjector.h b/projects/injection/public/SIREN/injection/CylinderVolumeLeptonInjector.h index 7f5ddc051..4cb2bd3d5 100644 --- a/projects/injection/public/SIREN/injection/CylinderVolumeLeptonInjector.h +++ b/projects/injection/public/SIREN/injection/CylinderVolumeLeptonInjector.h @@ -23,7 +23,6 @@ #include "SIREN/interactions/InteractionCollection.h" #include "SIREN/interactions/CrossSection.h" #include "SIREN/interactions/Decay.h" - #include "SIREN/detector/DetectorModel.h" #include "SIREN/distributions/primary/vertex/CylinderVolumePositionDistribution.h" #include "SIREN/geometry/Cylinder.h" // for Cylinder diff --git a/projects/injection/public/SIREN/injection/DecayRangeLeptonInjector.h b/projects/injection/public/SIREN/injection/DecayRangeLeptonInjector.h index d5d7868f0..26a0c5ec4 100644 --- a/projects/injection/public/SIREN/injection/DecayRangeLeptonInjector.h +++ b/projects/injection/public/SIREN/injection/DecayRangeLeptonInjector.h @@ -23,7 +23,6 @@ #include "SIREN/interactions/InteractionCollection.h" #include "SIREN/interactions/CrossSection.h" #include "SIREN/interactions/Decay.h" - #include "SIREN/detector/DetectorModel.h" #include "SIREN/distributions/primary/vertex/DecayRangeFunction.h" #include "SIREN/distributions/primary/vertex/DecayRangePositionDistribution.h" diff --git a/projects/injection/public/SIREN/injection/RangedLeptonInjector.h b/projects/injection/public/SIREN/injection/RangedLeptonInjector.h index fa50a3285..581aa91ae 100644 --- a/projects/injection/public/SIREN/injection/RangedLeptonInjector.h +++ b/projects/injection/public/SIREN/injection/RangedLeptonInjector.h @@ -21,7 +21,6 @@ #include "SIREN/interactions/InteractionCollection.h" #include "SIREN/interactions/CrossSection.h" #include "SIREN/interactions/Decay.h" - #include "SIREN/detector/DetectorModel.h" #include "SIREN/distributions/primary/vertex/RangeFunction.h" #include "SIREN/distributions/primary/vertex/RangePositionDistribution.h" diff --git a/projects/interactions/private/InteractionCollection.cxx b/projects/interactions/private/InteractionCollection.cxx index 015e3449c..af6f0e411 100644 --- a/projects/interactions/private/InteractionCollection.cxx +++ b/projects/interactions/private/InteractionCollection.cxx @@ -9,7 +9,7 @@ #include "SIREN/interactions/Interaction.h" // for Interaction #include "SIREN/interactions/CrossSection.h" // for CrossSe... -#include "SIREN/interactions/Decay.h" // for Decay +#include "SIREN/interactions/Decay.h" // for Decay #include "SIREN/dataclasses/InteractionRecord.h" // for Interac... #include "SIREN/dataclasses/InteractionSignature.h" // for Interac... #include "SIREN/dataclasses/Particle.h" // for Particle @@ -61,10 +61,6 @@ InteractionCollection::InteractionCollection(siren::dataclasses::ParticleType pr InitializeTargetTypes(); } - - - - InteractionCollection::InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> interactions) : primary_type(primary_type) { for(auto interaction : interactions) { std::shared_ptr xs = std::dynamic_pointer_cast(interaction); diff --git a/projects/interactions/private/pybindings/InteractionCollection.h b/projects/interactions/private/pybindings/InteractionCollection.h index 3262a2600..637724427 100644 --- a/projects/interactions/private/pybindings/InteractionCollection.h +++ b/projects/interactions/private/pybindings/InteractionCollection.h @@ -10,7 +10,6 @@ #include "../../public/SIREN/interactions/CrossSection.h" #include "../../public/SIREN/interactions/InteractionCollection.h" #include "../../public/SIREN/interactions/Decay.h" - #include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" #include "../../../geometry/public/SIREN/geometry/Geometry.h" #include "../../../utilities/public/SIREN/utilities/Random.h" diff --git a/projects/interactions/public/SIREN/interactions/InteractionCollection.h b/projects/interactions/public/SIREN/interactions/InteractionCollection.h index 2d2cb2d50..ecab54659 100644 --- a/projects/interactions/public/SIREN/interactions/InteractionCollection.h +++ b/projects/interactions/public/SIREN/interactions/InteractionCollection.h @@ -21,14 +21,13 @@ #include #include #include -#include +#include #include #include "SIREN/dataclasses/Particle.h" // for Particle #include "SIREN/interactions/CrossSection.h" #include "SIREN/interactions/Decay.h" - namespace siren { namespace dataclasses { class InteractionRecord; } } namespace siren { @@ -39,7 +38,6 @@ class InteractionCollection { siren::dataclasses::ParticleType primary_type; std::vector> cross_sections; std::vector> decays; - std::map>> cross_sections_by_target; std::set target_types; static const std::vector> empty; @@ -49,16 +47,13 @@ class InteractionCollection { virtual ~InteractionCollection() {}; InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> cross_sections); InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> decays); - InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> cross_sections, std::vector> decays); InteractionCollection(siren::dataclasses::ParticleType primary_type, std::vector> interactions); bool operator==(InteractionCollection const & other) const; std::vector> const & GetCrossSections() const {return cross_sections;} std::vector> const & GetDecays() const {return decays;} - bool const HasCrossSections() const {return cross_sections.size() > 0;} bool const HasDecays() const {return decays.size() > 0;} - std::vector> const & GetCrossSectionsForTarget(siren::dataclasses::ParticleType p) const; std::map>> const & GetCrossSectionsByTarget() const { return cross_sections_by_target; From 9d1c99a64ab278dfdbdac75a2c9a9ed57bedf503 Mon Sep 17 00:00:00 2001 From: Nicholas Kamp Date: Thu, 21 May 2026 14:00:47 -0400 Subject: [PATCH 55/93] remove extraneous comments --- projects/injection/private/Injector.cxx | 9 ------ projects/interactions/private/DMesonELoss.cxx | 31 ++----------------- .../private/QuarkDISFromSpline.cxx | 3 -- .../private/pybindings/DMesonELoss.h | 1 - .../public/SIREN/interactions/DMesonELoss.h | 3 +- python/SIREN_Controller.py | 8 ----- 6 files changed, 4 insertions(+), 51 deletions(-) diff --git a/projects/injection/private/Injector.cxx b/projects/injection/private/Injector.cxx index f352ca9c0..8edd54351 100644 --- a/projects/injection/private/Injector.cxx +++ b/projects/injection/private/Injector.cxx @@ -168,7 +168,6 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record throw(siren::utilities::InjectionFailure("No particle interaction!")); } - ////std::cout << "in sample cross section" << std::endl; std::set const & possible_targets = interactions->TargetTypes(); @@ -240,7 +239,6 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record } } } - //std::cout << "injector :: sample cross sections: after obtaining signatures" << std::endl; if(total_prob == 0) throw(siren::utilities::InjectionFailure("No valid interactions for this event!")); // Throw a random number @@ -261,16 +259,12 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record record.target_mass = detector_model->GetTargetMass(record.signature.target_type); siren::dataclasses::CrossSectionDistributionRecord xsec_record(record); if(r <= xsec_prob) { - //std::cout << "injector::sample cross section: going into sampel final state" << std::endl; matching_cross_sections[index]->SampleFinalState(xsec_record, random); - //std::cout << "injector::sample cross section: finished sampling" << std::endl; } else { matching_decays[index - matching_cross_sections.size()]->SampleFinalState(xsec_record, random); } - ////std::cout << "injector::sample cross section: calling finalizing" << std::endl; xsec_record.Finalize(record); - ////std::cout << "injector::sample cross section: finished finalizing" << std::endl; } @@ -312,12 +306,10 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { std::shared_ptr parent = tree.add_entry(record); // Secondary Processes - // std::cout << "injector::GenerateEvent : sampling secondary process" << std::endl; std::deque, std::shared_ptr>> secondaries; std::function)> add_secondaries = [&](std::shared_ptr parent) { for(size_t i=0; irecord.signature.secondary_types.size(); ++i) { siren::dataclasses::ParticleType const & type = parent->record.signature.secondary_types[i]; - // std::cout << "parent type" << parent->record.signature.secondary_types[i] << std::endl; std::map>::iterator it = secondary_process_map.find(type); if(it == secondary_process_map.end()) { @@ -349,7 +341,6 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { } catch(siren::utilities::InjectionFailure const & e) { return siren::dataclasses::InteractionTree(); } - //std::cout << "finished sampling secondary process" << std::endl; injected_events += 1; return tree; } diff --git a/projects/interactions/private/DMesonELoss.cxx b/projects/interactions/private/DMesonELoss.cxx index 4e95520bc..5242676b5 100644 --- a/projects/interactions/private/DMesonELoss.cxx +++ b/projects/interactions/private/DMesonELoss.cxx @@ -28,7 +28,7 @@ namespace interactions { DMesonELoss::DMesonELoss() { } - + bool DMesonELoss::equal(CrossSection const & other) const { const DMesonELoss* x = dynamic_cast(&other); @@ -65,7 +65,7 @@ std::vector DMesonELoss::GetPossibleSignature for(auto primary : primary_types_) { // hardcode the target type here, this should be fine std::vector new_signatures = GetPossibleSignaturesFromParents(primary, siren::dataclasses::Particle::ParticleType::PPlus); - signatures.insert(signatures.end(),new_signatures.begin(),new_signatures.end()); + signatures.insert(signatures.end(),new_signatures.begin(),new_signatures.end()); } return signatures; } @@ -89,17 +89,6 @@ std::vector DMesonELoss::GetPossibleSignature double DMesonELoss::TotalCrossSection(dataclasses::InteractionRecord const & interaction) const { siren::dataclasses::Particle::ParticleType primary_type = interaction.signature.primary_type; rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); - // rk::P4 p2(geom3::Vector3(interaction.target_momentum[1], interaction.target_momentum[2], interaction.target_momentum[3]), interaction.target_mass); - // double primary_energy; - - // // make sure of the reference frame before assigning energy - // if(interaction.target_momentum[1] == 0 and interaction.target_momentum[2] == 0 and interaction.target_momentum[3] == 0) { - // primary_energy = interaction.primary_momentum[0]; - // } else { - // rk::Boost boost_start_to_lab = p2.restBoost(); - // rk::P4 p1_lab = boost_start_to_lab * p1; - // primary_energy = p1_lab.e(); - // } double primary_energy = interaction.primary_momentum[0]; @@ -119,14 +108,8 @@ double DMesonELoss::TotalCrossSection(siren::dataclasses::Particle::ParticleType return xsec * mb_to_cm2; } -// double DMesonELoss::TotalCrossSection(siren::dataclasses::Particle::ParticleType primary_type, double primary_energy, siren::dataclasses::Particle::ParticleType target) const { -// return DMesonELoss::TotalCrossSection(primary_type,primary_energy); -// } - - double DMesonELoss::DifferentialCrossSection(dataclasses::InteractionRecord const & interaction) const { rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); - // rk::P4 p2(geom3::Vector3(interaction.target_momentum[1], interaction.target_momentum[2], interaction.target_momentum[3]), interaction.target_mass); double primary_energy; rk::P4 p1_lab; primary_energy = interaction.primary_momentum[0]; @@ -135,7 +118,7 @@ double DMesonELoss::DifferentialCrossSection(dataclasses::InteractionRecord cons double final_energy = interaction.secondary_momenta[0][0]; double z = 1 - final_energy / primary_energy; - + // now normalize the gaussian double total_xsec = TotalCrossSection(interaction.signature.primary_type, primary_energy); double z0 = 0.56; @@ -160,14 +143,6 @@ double DMesonELoss::InteractionThreshold(dataclasses::InteractionRecord const & void DMesonELoss::SampleFinalState(dataclasses::CrossSectionDistributionRecord& interaction, std::shared_ptr random) const { rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); - // rk::P4 p2(geom3::Vector3(interaction.target_momentum[1], interaction.target_momentum[2], interaction.target_momentum[3]), interaction.target_mass); - - // we assume that: - // the target is stationary so its energy is just its mass - // the incoming neutrino is massless, so its kinetic energy is its total energy - // double s = target_mass_ * tinteraction.secondary_momentarget_mass_ + 2 * target_mass_ * primary_energy; - // double s = std::pow(rk::invMass(p1, p2), 2); - double primary_energy; double Dmass = interaction.primary_mass; rk::P4 p1_lab; diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 4b5ca7206..8c323726c 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -813,7 +813,6 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR m1 *= pow(10.0, iteration); m3 *= pow(10.0, iteration); } - // pqy_lab = 0; } else {pqy_lab = std::sqrt(momq_lab*momq_lab - pqx_lab *pqx_lab);} if (std::isnan(pqy_lab)) { throw(siren::utilities::InjectionFailure( @@ -1007,8 +1006,6 @@ double QuarkDISFromSpline::FragmentationFraction(siren::dataclasses::Particle::P double QuarkDISFromSpline::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { // first compute the differential and total cross section double dxs = DifferentialCrossSection(interaction); - // if (dxs == 0) { - // } double txs = TotalCrossSection(interaction); // fragmentation fraction is now applied inside TotalCrossSection if(dxs == 0) { diff --git a/projects/interactions/private/pybindings/DMesonELoss.h b/projects/interactions/private/pybindings/DMesonELoss.h index 3956f0bd5..dc3462393 100644 --- a/projects/interactions/private/pybindings/DMesonELoss.h +++ b/projects/interactions/private/pybindings/DMesonELoss.h @@ -24,7 +24,6 @@ void register_DMesonELoss(pybind11::module_ & m) { .def(self == self) .def("TotalCrossSection",overload_cast(&DMesonELoss::TotalCrossSection, const_)) .def("TotalCrossSection",overload_cast(&DMesonELoss::TotalCrossSection, const_)) - // .def("DifferentialCrossSection",overload_cast(&DMesonELoss::DifferentialCrossSection, const_)) .def("InteractionThreshold",&DMesonELoss::InteractionThreshold) .def("GetPossibleTargets",&DMesonELoss::GetPossibleTargets) .def("GetPossibleTargetsFromPrimary",&DMesonELoss::GetPossibleTargetsFromPrimary) diff --git a/projects/interactions/public/SIREN/interactions/DMesonELoss.h b/projects/interactions/public/SIREN/interactions/DMesonELoss.h index 4244b5ec0..fd5d39f6e 100644 --- a/projects/interactions/public/SIREN/interactions/DMesonELoss.h +++ b/projects/interactions/public/SIREN/interactions/DMesonELoss.h @@ -47,8 +47,7 @@ friend cereal::access; double TotalCrossSection(dataclasses::InteractionRecord const &) const override; double TotalCrossSection(siren::dataclasses::Particle::ParticleType primary, double energy) const; - // double TotalCrossSection(siren::dataclasses::Particle::ParticleType primary, double energy, siren::dataclasses::Particle::ParticleType target) const override; - + double DifferentialCrossSection(dataclasses::InteractionRecord const &) const override; double InteractionThreshold(dataclasses::InteractionRecord const &) const override; void SampleFinalState(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr random) const override; diff --git a/python/SIREN_Controller.py b/python/SIREN_Controller.py index 2c9b7c4c1..7d32d8e5e 100644 --- a/python/SIREN_Controller.py +++ b/python/SIREN_Controller.py @@ -577,7 +577,6 @@ def GenerateEvents(self, N=None, fill_tables_at_exit=True, verbose=True): self.global_times.append(t-self.global_start) prev_time = t count += 1 - # print("finished generating one events") if hasattr(self, "DN_processes"): self.DN_processes.SaveCrossSectionTables(fill_tables_at_exit=fill_tables_at_exit) return self.events @@ -696,13 +695,6 @@ def SaveEvents(self, filename, fill_tables_at_exit=True, # weighter saving not yet supported self.weighter.SaveWeighter(filename) - # Add print statements to check the lengths of all datasets - # for key, value in datasets.items(): - # print(f"Length of {key}: {len(value)}") - # if isinstance(value[0], list): # If it's a list of lists, check the inner lengths - # for idx, sublist in enumerate(value): - # print(f" Length of {key}[{idx}]: {len(sublist)}") - # save events ak_array = ak.Array(datasets) if hdf5: From 799d53e043957aefc9018804d5592753a1d34822 Mon Sep 17 00:00:00 2001 From: Nicholas Kamp Date: Thu, 21 May 2026 14:30:29 -0400 Subject: [PATCH 56/93] one more whitespace reversion --- projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def | 2 -- 1 file changed, 2 deletions(-) diff --git a/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def b/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def index 800899b97..30b1fd838 100644 --- a/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def +++ b/projects/dataclasses/public/SIREN/dataclasses/ParticleTypes.def @@ -75,8 +75,6 @@ X(TauMinus, 15) X(NuTau, 16) X(NuTauBar, -16) - - /* Nuclei */ X(HNucleus, 1000010010) X(H2Nucleus, 1000010020) From 46b05421b3ca3b03dd783379f790d8a70b8e662a Mon Sep 17 00:00:00 2001 From: Nicholas Kamp <43788191+nickkamp1@users.noreply.github.com> Date: Thu, 21 May 2026 16:02:34 -0400 Subject: [PATCH 57/93] Apply suggestion from @austinschneider Co-authored-by: Austin Schneider --- projects/injection/private/Injector.cxx | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/injection/private/Injector.cxx b/projects/injection/private/Injector.cxx index 8edd54351..68ea809d9 100644 --- a/projects/injection/private/Injector.cxx +++ b/projects/injection/private/Injector.cxx @@ -12,7 +12,6 @@ #include "SIREN/interactions/DarkNewsCrossSection.h" #include "SIREN/interactions/InteractionCollection.h" #include "SIREN/interactions/Decay.h" - #include "SIREN/dataclasses/DecaySignature.h" #include "SIREN/dataclasses/InteractionSignature.h" #include "SIREN/dataclasses/Particle.h" From bc99e8c8fcbe2b6c0cf3442488cbc1fcbebb470b Mon Sep 17 00:00:00 2001 From: Nicholas Kamp Date: Thu, 21 May 2026 16:07:48 -0400 Subject: [PATCH 58/93] revert unncessary changes to Injector/Weighter logic --- projects/injection/private/Injector.cxx | 6 +----- projects/injection/private/Weighter.cxx | 3 --- projects/injection/private/WeightingUtils.cxx | 7 +++---- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/projects/injection/private/Injector.cxx b/projects/injection/private/Injector.cxx index 68ea809d9..1a1b8bd4a 100644 --- a/projects/injection/private/Injector.cxx +++ b/projects/injection/private/Injector.cxx @@ -167,7 +167,6 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record throw(siren::utilities::InjectionFailure("No particle interaction!")); } - std::set const & possible_targets = interactions->TargetTypes(); siren::math::Vector3D interaction_vertex( @@ -191,7 +190,6 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record std::vector matching_signatures; std::vector> matching_cross_sections; std::vector> matching_decays; - siren::dataclasses::InteractionRecord fake_record = record; double fake_prob; if (interactions->HasCrossSections()) { @@ -238,6 +236,7 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record } } } + if(total_prob == 0) throw(siren::utilities::InjectionFailure("No valid interactions for this event!")); // Throw a random number @@ -259,12 +258,10 @@ void Injector::SampleCrossSection(siren::dataclasses::InteractionRecord & record siren::dataclasses::CrossSectionDistributionRecord xsec_record(record); if(r <= xsec_prob) { matching_cross_sections[index]->SampleFinalState(xsec_record, random); - } else { matching_decays[index - matching_cross_sections.size()]->SampleFinalState(xsec_record, random); } xsec_record.Finalize(record); - } // Function to sample secondary processes @@ -309,7 +306,6 @@ siren::dataclasses::InteractionTree Injector::GenerateEvent() { std::function)> add_secondaries = [&](std::shared_ptr parent) { for(size_t i=0; irecord.signature.secondary_types.size(); ++i) { siren::dataclasses::ParticleType const & type = parent->record.signature.secondary_types[i]; - std::map>::iterator it = secondary_process_map.find(type); if(it == secondary_process_map.end()) { continue; diff --git a/projects/injection/private/Weighter.cxx b/projects/injection/private/Weighter.cxx index 87f36ce9e..4c9c389a9 100644 --- a/projects/injection/private/Weighter.cxx +++ b/projects/injection/private/Weighter.cxx @@ -25,8 +25,6 @@ #include "SIREN/injection/Process.h" // for Phy... #include "SIREN/injection/WeightingUtils.h" // for Cro... #include "SIREN/math/Vector3D.h" // for Vec... -#include "SIREN/dataclasses/Particle.h" - #include "SIREN/injection/Injector.h" @@ -133,7 +131,6 @@ double Weighter::EventWeight(siren::dataclasses::InteractionTree const & tree) c } } inv_weight += generation_probability / physical_probability; - } return 1./inv_weight; } diff --git a/projects/injection/private/WeightingUtils.cxx b/projects/injection/private/WeightingUtils.cxx index 2f9e27036..118e9139d 100644 --- a/projects/injection/private/WeightingUtils.cxx +++ b/projects/injection/private/WeightingUtils.cxx @@ -72,13 +72,12 @@ double CrossSectionProbability(std::shared_ptrGetTargetMass(target); // Add total cross section times density to the total prob - double total_xs = cross_section->TotalCrossSection(fake_record); - double target_prob = target_density * total_xs; + double target_prob = target_density * cross_section->TotalCrossSection(fake_record); total_prob += target_prob; // Add up total cross section times density times final state prob for matching signatures if(signature == record.signature) { - double final_prob = cross_section->FinalStateProbability(record); - selected_final_state += target_prob * final_prob; + // selected_prob += target_prob; + selected_final_state += target_prob * cross_section->FinalStateProbability(record); } } } From 3908809f6089b368af192064b31578efd0998a2f Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Thu, 21 May 2026 23:22:38 +0200 Subject: [PATCH 59/93] Apply suggestion from @austinschneider Co-authored-by: Austin Schneider --- projects/detector/private/DetectorModel.cxx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/projects/detector/private/DetectorModel.cxx b/projects/detector/private/DetectorModel.cxx index 5a740a8b9..92b6620a2 100644 --- a/projects/detector/private/DetectorModel.cxx +++ b/projects/detector/private/DetectorModel.cxx @@ -1017,13 +1017,9 @@ double DetectorModel::GetInteractionDepthInCGS(Geometry::IntersectionList const if(targets.empty()) { return distance / total_decay_length; // m / m --> dimensionless } - if(distance == 0.0) { + if(direction.magnitude() <= 1e-5) { return 0.0; } - if(direction.magnitude() <= distance_threshold) { - direction = intersections.direction; - // return 1e-05; // I have to do this to ensure that it works - } direction.normalize(); double dot = intersections.direction * direction; From 34d5dd94115db508b33d41252fde74eb4cc2f771 Mon Sep 17 00:00:00 2001 From: Miaochen Jin Date: Fri, 22 May 2026 05:29:53 -0400 Subject: [PATCH 60/93] revert changes to SIREN_Controller, minor edits to solve duplicate charm mass definition in constants.h --- .../private/QuarkDISFromSpline.cxx | 14 +- .../SIREN/interactions/QuarkDISFromSpline.h | 2 +- .../public/SIREN/utilities/Constants.h | 1 - python/SIREN_Controller.py | 139 +++++++++--------- 4 files changed, 77 insertions(+), 79 deletions(-) diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 8c323726c..8ac8dc0b8 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -60,7 +60,7 @@ bool kinematicallyAllowed(double xi, double y, double E, double M, double m_lep) if (y < 1e-9) return false; if (y > 1.0 - m_lep / E) return false; - const double mc = siren::utilities::Constants::CharmMass; + const double mc = siren::utilities::Constants::charmMass; const double Mch = siren::utilities::Constants::D0Mass; const double Q2 = slowRescalingQ2(xi, y, E, M, mc); @@ -511,7 +511,7 @@ double QuarkDISFromSpline::DifferentialCrossSection(dataclasses::InteractionReco double Q2 = -q.dot(q); double lepton_mass = GetLeptonMass(interaction.signature.secondary_types[lepton_index]); - const double mc = siren::utilities::Constants::CharmMass; + const double mc = siren::utilities::Constants::charmMass; double y = 1.0 - p2.dot(p3) / p2.dot(p1); // xi from inverting Q^2 = 2 M E y xi - m_c^2; guard against y <= 0: double xi = (y > 0.0) @@ -555,7 +555,7 @@ double QuarkDISFromSpline::DifferentialCrossSection(double energy, double xi, do if (std::isnan(Q2)) { Q2 = slowRescalingQ2(xi, y, energy, target_mass_, - siren::utilities::Constants::CharmMass); + siren::utilities::Constants::charmMass); } if (Q2 < minimum_Q2_) { return 0; @@ -616,7 +616,7 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR double E1_lab = p1_lab.e(); double E2_lab = p2_lab.e(); - const double mc = siren::utilities::Constants::CharmMass; + const double mc = siren::utilities::Constants::charmMass; const double Mch = siren::utilities::Constants::D0Mass; const double M_targ = target_mass_; const double E_nu = primary_energy; @@ -765,10 +765,10 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR record.interaction_parameters["bjorken_y"] = final_y; record.interaction_parameters["bjorken_x"] = xiToBjorkenX(final_xi, final_y, E1_lab, target_mass_, - siren::utilities::Constants::CharmMass); + siren::utilities::Constants::charmMass); double Q2 = slowRescalingQ2(final_xi, final_y, E1_lab, target_mass_, - siren::utilities::Constants::CharmMass); + siren::utilities::Constants::charmMass); double p1x_lab = std::sqrt(p1_lab.px() * p1_lab.px() + p1_lab.py() * p1_lab.py() + p1_lab.pz() * p1_lab.pz()); double pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); double momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); @@ -785,7 +785,7 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR // loop to resolve precision issue while (iteration <= maxIterations) { Q2 = slowRescalingQ2(final_xi, final_y, E1_lab, target_mass_, - siren::utilities::Constants::CharmMass); + siren::utilities::Constants::charmMass); p1x_lab = std::sqrt(p1_lab_x * p1_lab_x + p1_lab_y * p1_lab_y + p1_lab_z * p1_lab_z); pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); diff --git a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h index 66fde7224..d4cf9dd45 100644 --- a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h +++ b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h @@ -38,7 +38,7 @@ namespace interactions { /// Slow-rescaling charm DIS sampler. Expects FITS splines tabulated as /// dsdxidy(log10 E, log10 xi, log10 y), e.g. Maboi_M_Muon_SR/dsdxidy_*.fits. -/// Charm-quark mass m_c=1.27 GeV (Constants::CharmMass) and lightest charm +/// Charm-quark mass m_c=1.280 GeV (Constants::charmMass) and lightest charm /// hadron M_D0=1.86484 GeV (Constants::D0Mass) are taken from /// siren::utilities::Constants and are not configurable per-instance. class QuarkDISFromSpline : public CrossSection { diff --git a/projects/utilities/public/SIREN/utilities/Constants.h b/projects/utilities/public/SIREN/utilities/Constants.h index 1b31284a0..e558f41dc 100644 --- a/projects/utilities/public/SIREN/utilities/Constants.h +++ b/projects/utilities/public/SIREN/utilities/Constants.h @@ -68,7 +68,6 @@ static const double DsPlusMass = 1.96835; // GeV static const double DsMinusMass = 1.96835; // GeV static const double BPlusMass = 5.27932; // GeV static const double BMinusMass = 5.27932; // GeV -static const double CharmMass = 1.27; // GeV (charm-quark mass, meson-context legacy name) // Quark masses from https://pdg.lbl.gov/2020/reviews/rpp2020-rev-quark-masses.pdf static const double upMass = 0.00232; // GeV static const double downMass = 0.00471; // GeV diff --git a/python/SIREN_Controller.py b/python/SIREN_Controller.py index 7d32d8e5e..6086d6098 100644 --- a/python/SIREN_Controller.py +++ b/python/SIREN_Controller.py @@ -64,32 +64,20 @@ def __init__(self, events_to_inject, experiment=None, detector_model_file=None, # Empty list for our interaction trees self.events = [] - # Resolve detector model: either named experiment OR explicit file paths. - # PR #74 follow-up: previously this called _util.load_detector(experiment) - # unconditionally, which raised TypeError(model_regex.match(None)) when - # the caller intended to use explicit file overrides. - self._densities_file = None # used as fallback for fiducial volume - if detector_model_file is not None and materials_model_file is not None: - # Explicit-path branch: build DetectorModel directly from files - # (mirrors _util._detector_file_loader; do not call load_detector, - # which requires a named experiment). - self.detector_model = _detector.DetectorModel() - self.detector_model.LoadMaterialModel(materials_model_file) - self.detector_model.LoadDetectorModel(detector_model_file) - self._densities_file = detector_model_file - elif detector_model_file is None and materials_model_file is None: - if experiment is None: - raise ValueError( - "Must provide either `experiment` (named detector) or " - "both `detector_model_file` and `materials_model_file`." - ) - # Named-experiment branch: defer to upstream load_detector - self.detector_model = _util.load_detector(experiment) - else: - raise ValueError( - "Must provide both `detector_model_file` and `materials_model_file`, " - "or neither (and supply `experiment` instead)." - ) + + self.detector_model_file = detector_model_file + self.materials_model_file = materials_model_file + if experiment is not None: + # Find the density and materials files + detector_dir = _util.get_detector_model_path(experiment) + self.materials_model_file = os.path.join(detector_dir, "materials.dat") + self.detector_model_file = os.path.join(detector_dir, "densities.dat") + elif (self.detector_model_file is None or self.materials_model_file is None): + raise ValueError("Must provide either an experiment name or both a detector model file and materials model file") + + self.detector_model = _detector.DetectorModel() + self.detector_model.LoadMaterialModel(self.materials_model_file) + self.detector_model.LoadDetectorModel(self.detector_model_file) # Define the primary injection and physical process self.primary_injection_process = _injection.PrimaryInjectionProcess() @@ -125,35 +113,43 @@ def SetInjectionProcesses( :param bool fid_vol_secondary: whether to restrict secondary interactions to fiducial volume """ - # Define the primary injection process primary type. - # Upstream PR #71 replaced Add*Distribution(...) methods with a - # list-valued `distributions` property. We accumulate the full list - # locally then assign once. + # Define the primary injection process primary type self.primary_injection_process.primary_type = primary_type - primary_idist_list = [] + # Default injection distributions if "mass" not in primary_injection_distributions.keys(): - primary_idist_list.append(_distributions.PrimaryMass(0)) + self.primary_injection_process.AddPrimaryInjectionDistribution( + _distributions.PrimaryMass(0) + ) + if "helicity" not in primary_injection_distributions.keys(): - primary_idist_list.append(_distributions.PrimaryNeutrinoHelicityDistribution()) + self.primary_injection_process.AddPrimaryInjectionDistribution( + _distributions.PrimaryNeutrinoHelicityDistribution() + ) + + # Add all injection distributions for _, idist in primary_injection_distributions.items(): - primary_idist_list.append(idist) - self.primary_injection_process.distributions = primary_idist_list + self.primary_injection_process.AddPrimaryInjectionDistribution(idist) # Loop through possible secondary interactions for i_sec, secondary_type in enumerate(secondary_types): secondary_injection_process = _injection.SecondaryInjectionProcess() secondary_injection_process.primary_type = secondary_type - sec_idist_list = list(secondary_injection_distributions[i_sec]) + # Add all injection distributions + for idist in secondary_injection_distributions[i_sec]: + secondary_injection_process.AddSecondaryInjectionDistribution(idist) # Add the position distribution - if self.fid_vol is not None: - sec_idist_list.append(_distributions.SecondaryBoundedVertexDistribution(self.fid_vol)) + if fid_vol_secondary and self.fid_vol is not None: + secondary_injection_process.AddSecondaryInjectionDistribution( + _distributions.SecondaryBoundedVertexDistribution(self.fid_vol) + ) else: - sec_idist_list.append(_distributions.SecondaryPhysicalVertexDistribution()) + secondary_injection_process.AddSecondaryInjectionDistribution( + _distributions.SecondaryPhysicalVertexDistribution() + ) - secondary_injection_process.distributions = sec_idist_list self.secondary_injection_processes.append(secondary_injection_process) def SetPhysicalProcesses( @@ -171,25 +167,33 @@ def SetPhysicalProcesses( :param list secondary_physical_distributions: List of dict of physical distributions for each secondary process """ - # Define the primary physical process primary type. - # Upstream PR #71 replaced AddPhysicalDistribution(...) with the - # list-valued `distributions` property. + # Define the primary physical process primary type self.primary_physical_process.primary_type = primary_type - primary_pdist_list = [] + # Default physical distributions if "mass" not in primary_physical_distributions.keys(): - primary_pdist_list.append(_distributions.PrimaryMass(0)) + self.primary_physical_process.AddPhysicalDistribution( + _distributions.PrimaryMass(0) + ) + if "helicity" not in primary_physical_distributions.keys(): - primary_pdist_list.append(_distributions.PrimaryNeutrinoHelicityDistribution()) + self.primary_physical_process.AddPhysicalDistribution( + _distributions.PrimaryNeutrinoHelicityDistribution() + ) + + # Add all physical distributions for _, pdist in primary_physical_distributions.items(): - primary_pdist_list.append(pdist) - self.primary_physical_process.distributions = primary_pdist_list + self.primary_physical_process.AddPhysicalDistribution(pdist) # Loop through possible secondary interactions for i_sec, secondary_type in enumerate(secondary_types): secondary_physical_process = _injection.PhysicalProcess() secondary_physical_process.primary_type = secondary_type - secondary_physical_process.distributions = list(secondary_physical_distributions[i_sec]) + + # Add all physical distributions + for pdist in secondary_physical_distributions[i_sec]: + secondary_physical_process.AddPhysicalDistribution(pdist) + self.secondary_physical_processes.append(secondary_physical_process) def SetProcesses( @@ -292,14 +296,14 @@ def InputDarkNewsModel(self, primary_type, table_dir, upscattering=True, decay=T secondary_injection_process.primary_type = secondary_type # Add the secondary position distribution - if self.fid_vol is not None: - secondary_injection_process.distributions = [ + if fid_vol_secondary and self.fid_vol is not None: + secondary_injection_process.AddSecondaryInjectionDistribution( _distributions.SecondaryBoundedVertexDistribution(self.fid_vol) - ] + ) else: - secondary_injection_process.distributions = [ + secondary_injection_process.AddSecondaryInjectionDistribution( _distributions.SecondaryPhysicalVertexDistribution() - ] + ) if not inj_sec_defined: self.secondary_injection_processes.append(secondary_injection_process) @@ -361,26 +365,21 @@ def GetFiducialVolume(self): """ :return: identified fiducial volume for the experiment, None if not found """ - if self.experiment is not None: - return _util.get_fiducial_volume(self.experiment) - # Explicit-path branch: parse fiducial directly from the densities file - # (mirrors _util.get_fiducial_volume; avoids get_detector_model_path(None)). - if self._densities_file is None: - return None - with open(self._densities_file) as f: + with open(self.detector_model_file) as file: fiducial_line = None detector_line = None - for line in f: + for line in file: data = line.split() if len(data) <= 0: continue - if data[0] == "fiducial": + elif data[0] == "fiducial": fiducial_line = line elif data[0] == "detector": detector_line = line - if fiducial_line is None or detector_line is None: - return None - return _detector.DetectorModel.ParseFiducialVolume(fiducial_line, detector_line) + if fiducial_line is None or detector_line is None: + return None + return _detector.DetectorModel.ParseFiducialVolume(fiducial_line, detector_line) + return None def GetVolumePositionDistributionFromSector(self, sector_name): geo = self.GetDetectorSectorGeometry(sector_name) @@ -511,7 +510,7 @@ def InitializeInjector(self, filenames=None): assert(self.primary_injection_process.primary_type is not None) # Use controller injection objects self.injectors.append( - _injection._Injector( + _injection.Injector( self.events_to_inject, self.detector_model, self.primary_injection_process, @@ -524,7 +523,7 @@ def InitializeInjector(self, filenames=None): assert(len(filenames)>0) # require at least one injector filename for filename in filenames: self.injectors.append( - _injection._Injector( + _injection.Injector( self.events_to_inject, filename, self.random, @@ -538,7 +537,7 @@ def InitializeWeighter(self,filename=None): if filename is None: assert(self.primary_physical_process.primary_type is not None) # Use controller physical objects - self.weighter = _injection._Weighter( + self.weighter = _injection.Weighter( self.injectors, self.detector_model, self.primary_physical_process, @@ -546,7 +545,7 @@ def InitializeWeighter(self,filename=None): ) else: # Try initilalizing with the provided filename - self.weighter = _injection._Weighter( + self.weighter = _injection.Weighter( self.injectors, filename ) From 2c31202ed0dec0f92aa0be3a3fee4f88fdecf94a Mon Sep 17 00:00:00 2001 From: Nicholas Kamp Date: Wed, 27 May 2026 12:34:58 -0400 Subject: [PATCH 61/93] fix a latent hard-coded distance threshold --- projects/detector/private/DetectorModel.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/detector/private/DetectorModel.cxx b/projects/detector/private/DetectorModel.cxx index b7d573148..352674e0d 100644 --- a/projects/detector/private/DetectorModel.cxx +++ b/projects/detector/private/DetectorModel.cxx @@ -1233,7 +1233,7 @@ double DetectorModel::GetInteractionDepthInCGS(Geometry::IntersectionList const if(targets.empty()) { return distance / total_decay_length; // m / m --> dimensionless } - if(direction.magnitude() <= 1e-5) { + if(direction.magnitude() <= distance_threshold) { return 0.0; } direction.normalize(); From f1751c6ba202a87ef5e7c0d55a19799b4b479e05 Mon Sep 17 00:00:00 2001 From: Nicholas Kamp Date: Thu, 25 Jun 2026 17:52:15 -0400 Subject: [PATCH 62/93] remove photospline patches, gaurd against gcc < 9 --- cmake/Packages/photospline.cmake | 28 --- ...-bsplvb_simple-bspline_deriv_nonzero.patch | 163 ------------------ ...ove-cholmod-include-outside-extern-c.patch | 19 -- cmake/testing.cmake | 6 +- vendor/photospline | 2 +- 5 files changed, 6 insertions(+), 212 deletions(-) delete mode 100644 cmake/photospline_patches/0001-restore-bsplvb_simple-bspline_deriv_nonzero.patch delete mode 100644 cmake/photospline_patches/0002-move-cholmod-include-outside-extern-c.patch diff --git a/cmake/Packages/photospline.cmake b/cmake/Packages/photospline.cmake index 146e78a45..1b72f0fd8 100644 --- a/cmake/Packages/photospline.cmake +++ b/cmake/Packages/photospline.cmake @@ -21,34 +21,6 @@ endif() #add_subdirectory(${PROJECT_SOURCE_DIR}/vendor/photospline EXCLUDE_FROM_ALL) -# Apply local patches to the vendored photospline submodule before including -# it. Upstream photospline @ c6fb3ea (the pin used by SIREN) declares -# bsplvb_simple() and bspline_deriv_nonzero() in the public header but does -# not compile implementations for them -- the inline template code in -# detail/bspline_eval.h calls them, causing undefined-symbol link errors when -# libSIREN is loaded at runtime. Restore the implementations from an earlier -# photospline revision. Each patch is idempotent (--forward skips if already -# applied). -set(_PHOTOSPLINE_PATCH_DIR "${PROJECT_SOURCE_DIR}/cmake/photospline_patches") -if(EXISTS "${_PHOTOSPLINE_PATCH_DIR}") - file(GLOB _PHOTOSPLINE_PATCHES "${_PHOTOSPLINE_PATCH_DIR}/*.patch") - list(SORT _PHOTOSPLINE_PATCHES) - foreach(_patch IN LISTS _PHOTOSPLINE_PATCHES) - message(STATUS "Applying photospline patch: ${_patch}") - execute_process( - COMMAND patch -p1 --forward --silent -i "${_patch}" - WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/vendor/photospline" - RESULT_VARIABLE _patch_result - OUTPUT_QUIET ERROR_QUIET - ) - # patch returns 1 if already applied (which is fine); only error on 2+ - if(_patch_result GREATER 1) - message(FATAL_ERROR "Failed to apply photospline patch ${_patch} (exit ${_patch_result})") - endif() - endforeach() -endif() -unset(_PHOTOSPLINE_PATCH_DIR) -unset(_PHOTOSPLINE_PATCHES) # Override CMAKE_POLICY_VERSION_MINIMUM before adding subdirectory set(TEMP_CMAKE_POLICY_VERSION_MINIMUM ${CMAKE_POLICY_VERSION_MINIMUM}) diff --git a/cmake/photospline_patches/0001-restore-bsplvb_simple-bspline_deriv_nonzero.patch b/cmake/photospline_patches/0001-restore-bsplvb_simple-bspline_deriv_nonzero.patch deleted file mode 100644 index 08b356aeb..000000000 --- a/cmake/photospline_patches/0001-restore-bsplvb_simple-bspline_deriv_nonzero.patch +++ /dev/null @@ -1,163 +0,0 @@ -diff --git a/src/core/bspline.cpp b/src/core/bspline.cpp -index 9ac3a48..3e06d7c 100644 ---- a/src/core/bspline.cpp -+++ b/src/core/bspline.cpp -@@ -206,4 +206,158 @@ splineeval(const double *knots, const double *weights, int nknots, double x, int - // return cblas_sdot(totalcoeff, basis1->data, 1, basis2->data, 1); - //} - -+void -+bsplvb_simple(const double *knots, const unsigned nknots, -+ double x, int left, int degree, float* biatx) -+{ -+ int i, j; -+ double saved, term; -+ double delta_l[degree], delta_r[degree]; -+ -+ biatx[0] = 1.0; -+ -+ /* -+ * Handle the (rare) cases where x is outside the full -+ * support of the spline surface. -+ */ -+ if (left == degree-1) -+ while (left >= 0 && x < knots[left]) -+ left--; -+ else if (left == nknots-degree-1) -+ while (left < nknots-1 && x > knots[left+1]) -+ left++; -+ -+ /* -+ * NB: if left < degree-1 or left > nknots-degree-1, -+ * the following loop will dereference addresses ouside -+ * of knots[0:nknots]. While terms involving invalid knot -+ * indices will be discarded, it is important that `knots' -+ * have (maxdegree-1)*sizeof(double) bytes of padding -+ * before and after its valid range to prevent segfaults -+ * (see parsefitstable()). -+ */ -+ for (j = 0; j < degree-1; j++) { -+ delta_r[j] = knots[left+j+1] - x; -+ delta_l[j] = x - knots[left-j]; -+ -+ saved = 0.0; -+ -+ for (i = 0; i < j+1; i++) { -+ term = biatx[i] / (delta_r[i] + delta_l[j-i]); -+ biatx[i] = saved + delta_r[i]*term; -+ saved = delta_l[j-i]*term; -+ } -+ -+ biatx[j+1] = saved; -+ } -+ -+ /* -+ * If left < (spline order), only the first (left+1) -+ * splines are valid; the remainder are utter nonsense. -+ */ -+ if ((i = degree-1-left) > 0) { -+ for (j = 0; j < left+1; j++) -+ biatx[j] = biatx[j+i]; /* Move valid splines over. */ -+ for ( ; j < degree; j++) -+ biatx[j] = 0.0; /* The rest are zero by construction. */ -+ } else if ((i = left+degree+1-nknots) > 0) { -+ for (j = degree-1; j > i-1; j--) -+ biatx[j] = biatx[j-i]; -+ for ( ; j >= 0; j--) -+ biatx[j] = 0.0; -+ } -+} -+ -+void -+bsplvb(const double* knots, const double x, const int left, const int jlow, -+ const int jhigh, float* biatx, double* delta_l, double* delta_r) -+{ -+ int i, j; -+ double saved, term; -+ -+ if (jlow == 0) -+ biatx[0] = 1.0; -+ -+ for (j = jlow; j < jhigh-1; j++) { -+ delta_r[j] = knots[left+j+1] - x; -+ delta_l[j] = x - knots[left-j]; -+ -+ saved = 0.0; -+ -+ for (i = 0; i < j+1; i++) { -+ term = biatx[i] / (delta_r[i] + delta_l[j-i]); -+ biatx[i] = saved + delta_r[i]*term; -+ saved = delta_l[j-i]*term; -+ } -+ -+ biatx[j+1] = saved; -+ } -+} -+ -+ -+void -+bspline_deriv_nonzero(const double* knots, const unsigned nknots, -+ const double x, int left, const int n, float* biatx) -+{ -+ int i, j; -+ double temp, a; -+ double delta_l[n], delta_r[n]; -+ -+ /* Special case for constant splines */ -+ if (n == 0) -+ return; -+ -+ /* -+ * Handle the (rare) cases where x is outside the full -+ * support of the spline surface. -+ */ -+ if (left == n) -+ while (left >= 0 && x < knots[left]) -+ left--; -+ else if (left == nknots-n-2) -+ while (left < nknots-1 && x > knots[left+1]) -+ left++; -+ -+ /* Get the non-zero n-1th order B-splines at x */ -+ bsplvb(knots, x, left, 0 /* jlow */, n /* jhigh */, -+ biatx, delta_l, delta_r); -+ -+ /* -+ * Now, form the derivatives of the nth order B-splines from -+ * linear combinations of the lower-order splines. -+ */ -+ -+ /* -+ * On the last supported segment of the ith nth order spline, -+ * only the i+1th n-1th order spline is nonzero. -+ */ -+ temp = biatx[0]; -+ biatx[0] = - n*temp / ((knots[left+1] - knots[left+1-n])); -+ -+ /* On the middle segments, both the ith and i+1th splines contribute. */ -+ for (i = 1; i < n; i++) { -+ a = n*temp/((knots[left+i] - knots[left+i-n])); -+ temp = biatx[i]; -+ biatx[i] = a - n*temp/(knots[left+i+1] - knots[left+i+1-n]); -+ } -+ /* -+ * On the first supported segment of the i+nth nth order spline, -+ * only the ith n-1th order spline is nonzero. -+ */ -+ biatx[n] = n*temp/((knots[left+n] - knots[left])); -+ -+ /* Rearrange for partially-supported points. */ -+ if ((i = n-left) > 0) { -+ for (j = 0; j < left+1; j++) -+ biatx[j] = biatx[j+i]; /* Move valid splines over. */ -+ for ( ; j < n+1; j++) -+ biatx[j] = 0.0; /* The rest are zero by construction. */ -+ } else if ((i = left+n+2-nknots) > 0) { -+ for (j = n; j > i-1; j--) -+ biatx[j] = biatx[j-i]; -+ for ( ; j >= 0; j--) -+ biatx[j] = 0.0; -+ } -+ -+} - } //namespace photospline diff --git a/cmake/photospline_patches/0002-move-cholmod-include-outside-extern-c.patch b/cmake/photospline_patches/0002-move-cholmod-include-outside-extern-c.patch deleted file mode 100644 index a55eb4afa..000000000 --- a/cmake/photospline_patches/0002-move-cholmod-include-outside-extern-c.patch +++ /dev/null @@ -1,19 +0,0 @@ -diff --git a/include/photospline/detail/splineutil.h b/include/photospline/detail/splineutil.h -index bd7b985..828cbdc 100644 ---- a/include/photospline/detail/splineutil.h -+++ b/include/photospline/detail/splineutil.h -@@ -2,12 +2,12 @@ - #ifndef PHOTOSPLINE_CFITTER_SPLINEUTIL_H - #define PHOTOSPLINE_CFITTER_SPLINEUTIL_H - -+#include -+ - #ifdef __cplusplus - extern "C" { - #endif - --#include -- - struct ndsparse { - /* - * This is an ntuple, and is similar to the CHOLMOD triplet diff --git a/cmake/testing.cmake b/cmake/testing.cmake index 80097c2a6..455bf2329 100644 --- a/cmake/testing.cmake +++ b/cmake/testing.cmake @@ -16,7 +16,11 @@ if(${CIBUILDWHEEL}) else() macro(package_add_test TESTNAME) add_executable(${TESTNAME} ${ARGN}) - target_link_libraries(${TESTNAME} gtest gmock gtest_main SIREN) + # GCC < 9 keeps std::filesystem in a separate library (libstdc++fs) + # that must be linked explicitly; some tests (e.g. GDMLParser) use it + # directly. Newer GCC and Clang fold it into the standard library. + target_link_libraries(${TESTNAME} gtest gmock gtest_main SIREN + $<$,$,9.0>>:stdc++fs>) add_dependencies(${TESTNAME} rk_static) add_test(NAME ${TESTNAME} COMMAND ${TESTNAME} WORKING_DIRECTORY ${PROJECT_BINARY_DIR}) set_target_properties(${TESTNAME} PROPERTIES FOLDER tests) diff --git a/vendor/photospline b/vendor/photospline index c6fb3ea9a..53013f709 160000 --- a/vendor/photospline +++ b/vendor/photospline @@ -1 +1 @@ -Subproject commit c6fb3ea9a98f93705bc07b288b7f9264e621ac18 +Subproject commit 53013f709df16e2eb71a1b557a304718207ed46a From 4b7baf4acca1e0c9bf900a9571da8ff87c726ac2 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Fri, 26 Jun 2026 16:13:40 -0500 Subject: [PATCH 63/93] Remove TotalCrossSectionAllFinalStates override The TotalCrossSectionAllFinalStates override incorrectly reduces the total cross section. Removing the override reverts to the correct default implementation. Co-authored-by: Austin Schneider --- projects/interactions/private/PythiaDISCrossSection.cxx | 3 --- .../public/SIREN/interactions/PythiaDISCrossSection.h | 1 - 2 files changed, 4 deletions(-) diff --git a/projects/interactions/private/PythiaDISCrossSection.cxx b/projects/interactions/private/PythiaDISCrossSection.cxx index 50384eb1f..dd4066b25 100644 --- a/projects/interactions/private/PythiaDISCrossSection.cxx +++ b/projects/interactions/private/PythiaDISCrossSection.cxx @@ -307,9 +307,6 @@ double PythiaDISCrossSection::TotalCrossSection(siren::dataclasses::ParticleType return unit * std::pow(10.0, log_xs); } -double PythiaDISCrossSection::TotalCrossSectionAllFinalStates(dataclasses::InteractionRecord const & record) const { - return TotalCrossSection(record); -} double PythiaDISCrossSection::DifferentialCrossSection(dataclasses::InteractionRecord const & interaction) const { rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); diff --git a/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h b/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h index f3450ab0d..39bc166da 100644 --- a/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h +++ b/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h @@ -123,7 +123,6 @@ friend cereal::access; // Cross section from splines double TotalCrossSection(dataclasses::InteractionRecord const &) const override; double TotalCrossSection(siren::dataclasses::ParticleType primary, double energy) const; - double TotalCrossSectionAllFinalStates(dataclasses::InteractionRecord const &) const override; double DifferentialCrossSection(dataclasses::InteractionRecord const &) const override; double DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2=std::numeric_limits::quiet_NaN()) const; double InteractionThreshold(dataclasses::InteractionRecord const &) const override; From 10ca0635eff11b8b3f2ba293280652b510742751 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Fri, 26 Jun 2026 17:07:41 -0500 Subject: [PATCH 64/93] PythiaDISCrossSection: partition charm cross section by fragmentation fraction TotalCrossSection returned the full inclusive charm cross section for every registered D-type signature (D0, D+, Ds). Because the base-class TotalCrossSectionAllFinalStates and the Weighter both sum TotalCrossSection over those signatures, charm production was triple-counted, inflating the generation- and physical-side interaction depth by 3x. Apply FragmentationFraction(D_type) per signature so the sum reproduces the single inclusive charm cross section, matching QuarkDISFromSpline. The spurious TotalCrossSectionAllFinalStates override was already removed in the preceding commit. Add regression tests: CharmDISClosure_TEST (hermetic; exercises the real base-class TotalCrossSectionAllFinalStates default) and PythiaDISCharmClosure_TEST (real class, gated on Pythia8 support). --- projects/interactions/CMakeLists.txt | 13 ++ .../private/PythiaDISCrossSection.cxx | 26 ++- .../private/test/CharmDISClosure_TEST.cxx | 182 ++++++++++++++++++ .../test/PythiaDISCharmClosure_TEST.cxx | 118 ++++++++++++ 4 files changed, 333 insertions(+), 6 deletions(-) create mode 100644 projects/interactions/private/test/CharmDISClosure_TEST.cxx create mode 100644 projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx diff --git a/projects/interactions/CMakeLists.txt b/projects/interactions/CMakeLists.txt index a3f000249..ea1ae29e3 100644 --- a/projects/interactions/CMakeLists.txt +++ b/projects/interactions/CMakeLists.txt @@ -98,3 +98,16 @@ set_target_properties(interactions PROPERTIES LINK_FLAGS "-Wl,-rpath,\\\$ORIGIN/../siren.libs/") endif() package_add_test(UnitTest_CharmMesonDecay ${PROJECT_SOURCE_DIR}/projects/interactions/private/test/CharmMesonDecay_TEST.cxx) + +# Charm-DIS interaction-depth closure invariant (no Pythia8 needed: uses a mock +# that exercises the real base-class CrossSection::TotalCrossSectionAllFinalStates). +package_add_test(UnitTest_CharmDISClosure ${PROJECT_SOURCE_DIR}/projects/interactions/private/test/CharmDISClosure_TEST.cxx) + +# Real PythiaDISCrossSection charm-DIS closure/normalization regression test. +# Only built with Pythia8 support; needs charm-DIS spline files via env vars +# (SIREN_PYTHIA_TEST_DSDXDY, SIREN_PYTHIA_TEST_SIGMA), otherwise it SKIPs. +if(TARGET PYTHIA8) + package_add_test(UnitTest_PythiaDISCharmClosure ${PROJECT_SOURCE_DIR}/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx) + target_compile_definitions(UnitTest_PythiaDISCharmClosure PRIVATE SIREN_HAS_PYTHIA8) + target_link_libraries(UnitTest_PythiaDISCharmClosure PYTHIA8) +endif() diff --git a/projects/interactions/private/PythiaDISCrossSection.cxx b/projects/interactions/private/PythiaDISCrossSection.cxx index dd4066b25..c7b7eb5e0 100644 --- a/projects/interactions/private/PythiaDISCrossSection.cxx +++ b/projects/interactions/private/PythiaDISCrossSection.cxx @@ -289,7 +289,20 @@ double PythiaDISCrossSection::TotalCrossSection(dataclasses::InteractionRecord c siren::dataclasses::ParticleType primary_type = interaction.signature.primary_type; double primary_energy = interaction.primary_momentum[0]; if(primary_energy < InteractionThreshold(interaction)) return 0; - return TotalCrossSection(primary_type, primary_energy); + double total_xs = TotalCrossSection(primary_type, primary_energy); + // Partition the inclusive charm cross section across D species by fragmentation + // fraction for the specific D meson in this signature. The total spline holds the + // single inclusive charm cross section, so without this each of the registered + // D-type signatures (D0 + D+ + Ds) would return the full value and summing over + // them -- as the base-class TotalCrossSectionAllFinalStates and the Weighter do -- + // would triple-count charm production. Mirrors QuarkDISFromSpline::TotalCrossSection. + for(auto const & sec_type : interaction.signature.secondary_types) { + if(siren::dataclasses::isD(sec_type)) { + total_xs *= FragmentationFraction(sec_type); + break; + } + } + return total_xs; } double PythiaDISCrossSection::TotalCrossSection(siren::dataclasses::ParticleType primary_type, double primary_energy) const { @@ -377,11 +390,12 @@ double PythiaDISCrossSection::FragmentationFraction(siren::dataclasses::Particle } double PythiaDISCrossSection::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { - // Trust Pythia: SampleFinalState accepts whatever charm meson Pythia - // produces and overwrites the signature's meson_type to match. The natural - // Lund-string fragmentation distribution IS the physical fragfrac, so we - // don't multiply by a PDG-average fragfrac table here — that would double- - // count. Return dσ/σ only (matches DISFromSpline). + // Return dsigma/sigma. The fragmentation fraction is now applied inside + // TotalCrossSection (per signature), so txs below already carries it; it + // cancels against the per-signature TotalCrossSection weight in + // CrossSectionProbability, leaving the correct kinematic density. We do NOT + // multiply by a fragfrac table here -- SampleFinalState trusts Pythia's + // natural Lund-string fragmentation for the actual D-type distribution. double dxs = DifferentialCrossSection(interaction); double txs = TotalCrossSection(interaction); if (!std::isfinite(dxs) || !std::isfinite(txs) || dxs <= 0 || txs <= 0) return 0.0; diff --git a/projects/interactions/private/test/CharmDISClosure_TEST.cxx b/projects/interactions/private/test/CharmDISClosure_TEST.cxx new file mode 100644 index 000000000..ace3ea617 --- /dev/null +++ b/projects/interactions/private/test/CharmDISClosure_TEST.cxx @@ -0,0 +1,182 @@ +// Regression test for the charm-DIS interaction-depth closure invariant. +// +// Background: a charm-DIS cross section registers three D-type final-state +// signatures (D0, D+, Ds) that share a single inclusive charm total cross +// section sigma(E). The interaction depth that sets the vertex distribution is +// computed two ways that MUST agree: +// +// generation side : CrossSection::TotalCrossSectionAllFinalStates(record) +// (called by the ~10 vertex/position distributions) +// physical side : sum over GetPossibleSignaturesFromParents of +// TotalCrossSection(signature) (Weighter.tcc) +// +// The base-class default TotalCrossSectionAllFinalStates SUMS TotalCrossSection +// over the signatures, so the two sides agree only if no subclass overrides +// TotalCrossSectionAllFinalStates to short-circuit the sum. Additionally, the +// inclusive sigma must be PARTITIONED across the D species by fragmentation +// fraction, otherwise the sum triple-counts charm production. +// +// This test does not need Pythia8: it uses a mock that reproduces the two +// behaviors of PythiaDISCrossSection (TotalCrossSection independent of meson +// type; three registered D-type signatures) and exercises the real base-class +// CrossSection::TotalCrossSectionAllFinalStates. It guards the invariant the +// PythiaDISCrossSection fix relies on. The companion gtest +// PythiaDISCharmClosure_TEST exercises the real class on a Pythia-enabled build. + +#include +#include +#include +#include + +#include + +#include "SIREN/interactions/CrossSection.h" +#include "SIREN/dataclasses/Particle.h" +#include "SIREN/dataclasses/InteractionRecord.h" +#include "SIREN/dataclasses/InteractionSignature.h" + +using namespace siren::interactions; +using namespace siren::dataclasses; + +namespace { + +// Mock reproducing PythiaDISCrossSection's relevant semantics. +// apply_ff : multiply by FragmentationFraction per signature (the fix / QuarkDIS) +// override_afs : short-circuit TotalCrossSectionAllFinalStates to TotalCrossSection +// (the spurious f1751c6b override) +class MockCharmXS : public CrossSection { +public: + bool apply_ff; + bool override_afs; + MockCharmXS(bool ff, bool ovr) : apply_ff(ff), override_afs(ovr) {} + + static double FragmentationFraction(ParticleType d) { + if(d==ParticleType::D0 || d==ParticleType::D0Bar) return 0.6; + if(d==ParticleType::DPlus || d==ParticleType::DMinus) return 0.23; + if(d==ParticleType::DsPlus || d==ParticleType::DsMinus) return 0.15; + return 0.0; + } + // Stand-in for the 1-D inclusive charm total spline (independent of meson type). + static double sigma_inclusive(double E) { return 1.0e-38 * std::log10(E); } + + std::vector sigs(ParticleType primary, ParticleType target) const { + InteractionSignature base; + base.primary_type = primary; + base.target_type = target; + base.secondary_types = { ParticleType::MuMinus, ParticleType::D0 }; + std::vector out; + for(ParticleType d : { ParticleType::D0, ParticleType::DPlus, ParticleType::DsPlus }) { + InteractionSignature s = base; + s.secondary_types[1] = d; + out.push_back(s); + } + return out; + } + + double TotalCrossSection(InteractionRecord const & r) const override { + double s = sigma_inclusive(r.primary_momentum[0]); + if(apply_ff) { + for(auto t : r.signature.secondary_types) + if(isD(t)) { s *= FragmentationFraction(t); break; } + } + return s; + } + double TotalCrossSectionAllFinalStates(InteractionRecord const & r) const override { + if(override_afs) return TotalCrossSection(r); + return CrossSection::TotalCrossSectionAllFinalStates(r); + } + std::vector GetPossibleSignaturesFromParents(ParticleType p, ParticleType t) const override { + return sigs(p, t); + } + std::vector GetPossibleSignatures() const override { return sigs(ParticleType::NuMu, ParticleType::PPlus); } + std::vector GetPossibleTargets() const override { return { ParticleType::PPlus }; } + std::vector GetPossibleTargetsFromPrimary(ParticleType) const override { return { ParticleType::PPlus }; } + std::vector GetPossiblePrimaries() const override { return { ParticleType::NuMu }; } + double DifferentialCrossSection(InteractionRecord const &) const override { return 0.0; } + double InteractionThreshold(InteractionRecord const &) const override { return 0.0; } + double FinalStateProbability(InteractionRecord const &) const override { return 0.0; } + std::vector DensityVariables() const override { return {}; } + void SampleFinalState(CrossSectionDistributionRecord &, std::shared_ptr) const override {} + bool equal(CrossSection const &) const override { return false; } +}; + +// Replicates the Weighter.tcc physical-side loop (InteractionProbability, +// NormalizedPositionProbability) and WeightingUtils CrossSectionProbability: +// sum TotalCrossSection over GetPossibleSignaturesFromParents. +double phys_path(MockCharmXS const & xs, ParticleType primary, ParticleType target, double E) { + InteractionRecord fake; + fake.signature.primary_type = primary; + fake.signature.target_type = target; + fake.primary_momentum[0] = E; + double total = 0.0; + for(auto const & sig : xs.GetPossibleSignaturesFromParents(primary, target)) { + fake.signature = sig; + fake.primary_momentum[0] = E; + total += xs.TotalCrossSection(fake); + } + return total; +} + +double gen_path(MockCharmXS const & xs, ParticleType primary, ParticleType target, double E) { + InteractionRecord rec; + rec.signature.primary_type = primary; + rec.signature.target_type = target; + rec.primary_momentum[0] = E; + return xs.TotalCrossSectionAllFinalStates(rec); +} + +} // namespace + +// The base-class default must sum TotalCrossSection over the registered +// signatures: three D-type signatures sharing sigma -> 3*sigma. +TEST(CharmDISClosure, BaseDefaultSumsOverSignatures) { + MockCharmXS xs(/*ff=*/false, /*override=*/false); + const double E = 100.0; + const double s = MockCharmXS::sigma_inclusive(E); + EXPECT_NEAR(gen_path(xs, ParticleType::NuMu, ParticleType::PPlus, E), 3.0 * s, 1e-50); +} + +// Reproduces the f1751c6b bug: the override makes the generation side report 1x +// while the physical side reports 3x -> closure broken by a factor of 3. +TEST(CharmDISClosure, OverrideBreaksClosureByFactorThree) { + MockCharmXS xs(/*ff=*/false, /*override=*/true); + for(double E : {10.0, 100.0, 1000.0}) { + double gen = gen_path(xs, ParticleType::NuMu, ParticleType::PPlus, E); + double phys = phys_path(xs, ParticleType::NuMu, ParticleType::PPlus, E); + EXPECT_NEAR(gen, MockCharmXS::sigma_inclusive(E), 1e-50); // 1x + EXPECT_NEAR(phys, 3.0 * MockCharmXS::sigma_inclusive(E), 1e-50); // 3x + EXPECT_NEAR(gen / phys, 1.0 / 3.0, 1e-9); + } +} + +// Reproduces 4b7baf4a (override removed, no FF): the two sides agree (closure +// restored) but both equal 3x the inclusive sigma -> charm production overcounted. +TEST(CharmDISClosure, OverrideRemovedClosesButOvercountsThreeX) { + MockCharmXS xs(/*ff=*/false, /*override=*/false); + for(double E : {10.0, 100.0, 1000.0}) { + double gen = gen_path(xs, ParticleType::NuMu, ParticleType::PPlus, E); + double phys = phys_path(xs, ParticleType::NuMu, ParticleType::PPlus, E); + EXPECT_NEAR(gen, phys, 1e-50); // closure ok + EXPECT_NEAR(gen, 3.0 * MockCharmXS::sigma_inclusive(E), 1e-50); // but 3x high + } +} + +// The proposed fix (override removed + fragmentation fraction in TotalCrossSection): +// the two sides agree AND both equal sigma*(0.6+0.23+0.15) = 0.98*sigma -- the +// inclusive charm cross section, partitioned not triple-counted. +TEST(CharmDISClosure, FragmentationFractionRestoresPhysicalNormalization) { + MockCharmXS xs(/*ff=*/true, /*override=*/false); + const double ff_sum = 0.6 + 0.23 + 0.15; + for(double E : {10.0, 100.0, 1000.0}) { + double gen = gen_path(xs, ParticleType::NuMu, ParticleType::PPlus, E); + double phys = phys_path(xs, ParticleType::NuMu, ParticleType::PPlus, E); + double s = MockCharmXS::sigma_inclusive(E); + EXPECT_NEAR(gen, phys, 1e-50); // closure ok + EXPECT_NEAR(gen, ff_sum * s, 1e-48); // and physically normalized (~sigma) + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx b/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx new file mode 100644 index 000000000..a46ea38fa --- /dev/null +++ b/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx @@ -0,0 +1,118 @@ +// Regression test for the PythiaDISCrossSection charm-DIS interaction-depth +// closure / normalization bug. +// +// Requires a Pythia8-enabled build (SIREN_WITH_PYTHIA8=ON) AND total/differential +// charm-DIS spline files supplied via environment variables: +// +// SIREN_PYTHIA_TEST_DSDXDY -> differential (dsdxdy) FITS spline +// SIREN_PYTHIA_TEST_SIGMA -> total (sigma) FITS spline +// +// If the variables are unset the test SKIPs (it only needs the spline tables, +// not LHAPDF/Pythia at runtime: the total-cross-section path never calls Pythia). +// +// What it guards: +// * Per-signature TotalCrossSection must be PARTITIONED by fragmentation +// fraction (D0:0.6, D+:0.23, Ds:0.15), not equal to the full inclusive sigma. +// * sum over signatures of TotalCrossSection == sigma_inclusive * 0.98 (NOT 3x). +// * TotalCrossSectionAllFinalStates (generation side) == that sum (physical +// side) -> closure holds. +// +// Before the fix (no fragmentation fraction in TotalCrossSection) every signature +// returns the full inclusive sigma, the sum is 3x too large, and the partition +// assertions FAIL. After the fix they PASS. + +#include +#include +#include +#include +#include + +#include + +#include "SIREN/interactions/CrossSection.h" +#include "SIREN/dataclasses/Particle.h" +#include "SIREN/dataclasses/InteractionRecord.h" +#include "SIREN/dataclasses/InteractionSignature.h" + +#ifdef SIREN_HAS_PYTHIA8 +#include "SIREN/interactions/PythiaDISCrossSection.h" +#endif + +using namespace siren::interactions; +using namespace siren::dataclasses; + +namespace { +double expected_ff(ParticleType d) { + if(d==ParticleType::D0 || d==ParticleType::D0Bar) return 0.6; + if(d==ParticleType::DPlus || d==ParticleType::DMinus) return 0.23; + if(d==ParticleType::DsPlus || d==ParticleType::DsMinus) return 0.15; + return 0.0; +} +} // namespace + +#ifdef SIREN_HAS_PYTHIA8 +TEST(PythiaDISCharmClosure, FragmentationPartitionAndClosure) { + const char* dd = std::getenv("SIREN_PYTHIA_TEST_DSDXDY"); + const char* tt = std::getenv("SIREN_PYTHIA_TEST_SIGMA"); + if(!dd || !tt) { + GTEST_SKIP() << "Set SIREN_PYTHIA_TEST_DSDXDY and SIREN_PYTHIA_TEST_SIGMA to run."; + } + + std::vector primaries = { ParticleType::NuMu }; + std::vector targets = { ParticleType::PPlus }; + PythiaDISCrossSection xs( + std::string(dd), std::string(tt), + /*interaction_type=*/1, /*target_mass=*/0.9382720813, + /*minimum_Q2=*/1.0, primaries, targets, + /*pythia_data_path=*/"", /*pdf_set=*/"LHAPDF6:CT18NLO", /*units=*/"cm"); + + const double ff_sum = 0.6 + 0.23 + 0.15; + + for(double E : {30.0, 100.0, 300.0}) { + double sigma_incl = xs.TotalCrossSection(ParticleType::NuMu, E); + ASSERT_GT(sigma_incl, 0.0); + + std::vector sigs = + xs.GetPossibleSignaturesFromParents(ParticleType::NuMu, ParticleType::PPlus); + ASSERT_EQ(sigs.size(), 3u); + + double sum = 0.0; + std::vector per_sig; + for(auto const & sig : sigs) { + InteractionRecord rec; + rec.signature = sig; + rec.primary_momentum[0] = E; + double v = xs.TotalCrossSection(rec); + sum += v; + per_sig.push_back(v); + + // identify the D meson in this signature and check FF partition + ParticleType d = ParticleType::unknown; + for(auto t : sig.secondary_types) if(isD(t)) { d = t; break; } + ASSERT_NE(d, ParticleType::unknown); + EXPECT_NEAR(v, sigma_incl * expected_ff(d), sigma_incl * 1e-9) + << "signature D-type cross section is not fragmentation-partitioned at E=" << E; + } + + // The three values must differ (0.6 vs 0.23 vs 0.15), i.e. NOT all == sigma_incl. + EXPECT_GT(std::abs(per_sig[0] - per_sig[1]), sigma_incl * 1e-6); + + // Sum is the inclusive sigma (partitioned), not 3x. + EXPECT_NEAR(sum, sigma_incl * ff_sum, sigma_incl * 1e-9); + EXPECT_LT(sum, sigma_incl * 1.5); // would be 3x without the fix + + // Generation side (TotalCrossSectionAllFinalStates) == physical side (sum). + InteractionRecord rec; + rec.signature.primary_type = ParticleType::NuMu; + rec.signature.target_type = ParticleType::PPlus; + rec.primary_momentum[0] = E; + double gen = xs.TotalCrossSectionAllFinalStates(rec); + EXPECT_NEAR(gen, sum, sigma_incl * 1e-9) << "generation/physical interaction depth mismatch"; + } +} +#endif // SIREN_HAS_PYTHIA8 + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From 497d2ab38f05dd5dda74ac03bd1e507b909e0be9 Mon Sep 17 00:00:00 2001 From: Nicholas Kamp Date: Fri, 26 Jun 2026 19:17:30 -0400 Subject: [PATCH 65/93] raise error instead of using FASRC lhapdf path --- projects/interactions/private/PythiaDISCrossSection.cxx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/projects/interactions/private/PythiaDISCrossSection.cxx b/projects/interactions/private/PythiaDISCrossSection.cxx index c7b7eb5e0..928301217 100644 --- a/projects/interactions/private/PythiaDISCrossSection.cxx +++ b/projects/interactions/private/PythiaDISCrossSection.cxx @@ -443,9 +443,10 @@ void PythiaDISCrossSection::InitializePythia(double E_nu, int target_pdg) const // The pdf_set_ is e.g. "LHAPDF6:HERAPDF20_NLO_EIG", and LHAPDF needs LHAPDF_DATA_PATH set const char* lhapdf_path = std::getenv("LHAPDF_DATA_PATH"); if (!lhapdf_path) { - // Try a reasonable default based on common install layouts - std::string default_path = "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/pzhelnin/LHAPDF/new_install/share/LHAPDF"; - setenv("LHAPDF_DATA_PATH", default_path.c_str(), 0); + throw std::runtime_error("LHAPDF_DATA_PATH is not set"); + // Commented code below sets a default path on the Harvard FASRC cluster + // std::string default_path = "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/pzhelnin/LHAPDF/new_install/share/LHAPDF"; + // setenv("LHAPDF_DATA_PATH", default_path.c_str(), 0); } pythia_ = std::make_unique(pythia_data_path_, false); From 58c36a1dde8a8e345f5674ba700561ddf9930ee6 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Fri, 26 Jun 2026 18:44:28 -0500 Subject: [PATCH 66/93] Fix degenerate trajectory-direction handling in DetectorModel/Vector3D Guard Vector3D::normalize() against zero length so a degenerate (p1 - p0) cannot produce NaN. Return the decay term from the sub-threshold interaction-depth guard instead of 0 and drop the redundant exact-zero check in the interaction-density guard. --- projects/detector/private/DetectorModel.cxx | 6 +++--- projects/math/private/Vector3D.cxx | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/projects/detector/private/DetectorModel.cxx b/projects/detector/private/DetectorModel.cxx index 352674e0d..9628136a7 100644 --- a/projects/detector/private/DetectorModel.cxx +++ b/projects/detector/private/DetectorModel.cxx @@ -923,7 +923,7 @@ double DetectorModel::GetInteractionDensity(Geometry::IntersectionList const & i std::vector const & total_cross_sections, double const & total_decay_length) const { Vector3D direction = p0 - intersections.position; - if(direction.magnitude() == 0 || direction.magnitude() <= distance_threshold) { + if(direction.magnitude() <= distance_threshold) { direction = intersections.direction; } else { direction.normalize(); @@ -1233,8 +1233,8 @@ double DetectorModel::GetInteractionDepthInCGS(Geometry::IntersectionList const if(targets.empty()) { return distance / total_decay_length; // m / m --> dimensionless } - if(direction.magnitude() <= distance_threshold) { - return 0.0; + if(distance <= distance_threshold) { + return distance / total_decay_length; } direction.normalize(); diff --git a/projects/math/private/Vector3D.cxx b/projects/math/private/Vector3D.cxx index a820eb7c7..9bbfdddc7 100644 --- a/projects/math/private/Vector3D.cxx +++ b/projects/math/private/Vector3D.cxx @@ -265,6 +265,9 @@ double Vector3D::magnitude() const void Vector3D::normalize() { double length = std::sqrt(cartesian_.x_ * cartesian_.x_ + cartesian_.y_ * cartesian_.y_ + cartesian_.z_ * cartesian_.z_); + if(length == 0.0) { + return; + } cartesian_.x_ = cartesian_.x_ / length; cartesian_.y_ = cartesian_.y_ / length; cartesian_.z_ = cartesian_.z_ / length; From 9b8517ae2f001532aacd9c60822636be01451f96 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 10:59:07 -0500 Subject: [PATCH 67/93] QuarkDISFromSpline: fix precision-loop kinematics, sampling robustness, fragmentation normalization Replace the power-of-10 precision-rescaling loop in SampleFinalState with a scale-free closed form that does not corrupt the q 4-vector. Add the pqy^2 >= 0 constraint to kinematicallyAllowed so the sampler and density share one support, throw InjectionFailure on trial-cap exhaustion, reject the unsupported electron-target signature, and renormalize the fragmentation fractions to sum to 1. --- .../private/QuarkDISFromSpline.cxx | 178 ++++++++++++------ .../private/test/CharmDISClosure_TEST.cxx | 30 ++- .../SIREN/interactions/QuarkDISFromSpline.h | 13 ++ 3 files changed, 152 insertions(+), 69 deletions(-) diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 8ac8dc0b8..ce693e389 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -69,6 +69,25 @@ bool kinematicallyAllowed(double xi, double y, double E, double M, double m_lep) const double W2 = slowRescalingW2(xi, y, E, M, mc); if (W2 <= (M + Mch) * (M + Mch)) return false; + // Transverse-momentum balance: the exchanged q must have a real transverse + // component pqy in the lab frame. This is the SAME q-decomposition the sampler + // uses (SampleFinalState), so adding it here makes kinematicallyAllowed the + // single predicate shared by the sampler proposal loops and by + // DifferentialCrossSection/FinalStateProbability. Without it the density + // (dxs/txs) is nonzero on points the sampler must reject (pqy^2 < 0), + // breaking Sample==Density closure and silently biasing low-Bjorken-x events. + // + // Primary is a neutrino here (InitializeSignatures enforces isNeutrino), so + // m1 = 0 and |p1| = E. Using P1 = E (massless primary) to match the sampler. + // pqy^2 = momq^2 - pqx^2 with: + // pqx = (m_lep^2 + 2 P1^2 + Q2 + 2 E^2 (y-1)) / (2 P1) + // momq^2 = P1^2 + Q2 + E^2 (y^2 - 1) + const double P1 = E; + const double pqx = (m_lep * m_lep + 2.0 * P1 * P1 + Q2 + 2.0 * E * E * (y - 1.0)) / (2.0 * P1); + const double momq2 = P1 * P1 + Q2 + E * E * (y * y - 1.0); + const double pqy2 = momq2 - pqx * pqx; + if (pqy2 < 0.0) return false; + return true; } } @@ -236,9 +255,9 @@ double QuarkDISFromSpline::getHadronMass(siren::dataclasses::ParticleType hadron case siren::dataclasses::ParticleType::DMinus: return( siren::utilities::Constants::DPlusMass); case siren::dataclasses::ParticleType::DsPlus: - return 1.96834; // GeV (PDG 2022); no Constants::DsMass yet + return( siren::utilities::Constants::DsPlusMass); case siren::dataclasses::ParticleType::DsMinus: - return 1.96834; + return( siren::utilities::Constants::DsMinusMass); default: return(0.0); } @@ -246,7 +265,9 @@ double QuarkDISFromSpline::getHadronMass(siren::dataclasses::ParticleType hadron std::map QuarkDISFromSpline::getIndices(siren::dataclasses::InteractionSignature signature) { - int lepton_id, hadron_id, meson_id; + // Initialize to -1 so an unmatched slot is detectable instead of being read + // back as an uninitialized index into the secondary arrays (UB / OOB). + int lepton_id = -1, hadron_id = -1, meson_id = -1; for (size_t i = 0; i < signature.secondary_types.size(); i++){ if (isLepton(signature.secondary_types[i])) { lepton_id = i; @@ -259,6 +280,12 @@ std::map QuarkDISFromSpline::getIndices(siren::dataclasses::In continue; } } + // A valid charm-DIS signature is (lepton, hadron, D-meson); a missing slot + // (e.g. the malformed {Hadrons,Hadrons,D} signature with no lepton) would + // otherwise leave an index at -1 and corrupt downstream array access. + if (lepton_id < 0 || hadron_id < 0 || meson_id < 0) { + throw std::runtime_error("QuarkDISFromSpline::getIndices: signature does not contain the expected (lepton, hadron, D-meson) triplet"); + } return {{"lepton", lepton_id}, {"hadron", hadron_id}, {"meson", meson_id}}; } @@ -353,7 +380,12 @@ void QuarkDISFromSpline::InitializeSignatures() { } else if(interaction_type_ == 2) { signature.secondary_types.push_back(neutral_lepton_product); } else if(interaction_type_ == 3) { - signature.secondary_types.push_back(siren::dataclasses::ParticleType::Hadrons); + // interaction_type 3 (electron / e-target) has no outgoing lepton in + // this charm convention and would emit a malformed {Hadrons,Hadrons,D} + // signature with no lepton index. The differential cross section and + // sampler both require a lepton to form q = p1 - p3, so reject it at + // construction rather than create an unusable signature. + throw std::runtime_error("QuarkDISFromSpline: interaction_type 3 (e-target) not supported for charm D-meson production; use 1 (CC) or 2 (NC)"); } else { throw std::runtime_error("InitializeSignatures: Unkown interaction type!"); } @@ -672,7 +704,9 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR double trials = 0; double xi_trial = 0.0, y_trial = 0.0; do { - if (trials >= 100) throw std::runtime_error("too much trials"); + // Per-event recoverable failure: drop this event (InjectionFailure is + // caught by the Injector) rather than aborting the whole run. + if (trials >= 100) throw siren::utilities::InjectionFailure("QuarkDISFromSpline: initial rejection sampling failed to find a kinematically allowed point in 100 trials"); trials += 1; kin_vars[1] = random->Uniform(logXiMin, 0.0); kin_vars[2] = random->Uniform(logYMin, logYMax); @@ -717,8 +751,10 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR double xi_trial = 0.0, y_trial = 0.0; int burnin_trials = 0; do { + // Per-event recoverable failure: drop this event (InjectionFailure is + // caught by the Injector) rather than aborting the whole run. if (++burnin_trials >= 100) - throw std::runtime_error("QuarkDISFromSpline: burn-in proposal failed to find allowed point in 100 trials"); + throw siren::utilities::InjectionFailure("QuarkDISFromSpline: burn-in proposal failed to find allowed point in 100 trials"); test_kin_vars[1] = random->Uniform(logXiMin, 0.0); test_kin_vars[2] = random->Uniform(logYMin, logYMax); xi_trial = std::pow(10., test_kin_vars[1]); @@ -769,56 +805,39 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR double Q2 = slowRescalingQ2(final_xi, final_y, E1_lab, target_mass_, siren::utilities::Constants::charmMass); + // Scale-free closed form for the exchanged q decomposition (replaces the old + // /10 precision-rescaling loop, which was unsound: slowRescalingQ2 holds the + // target/charm masses fixed, so Q2 is NOT homogeneous of degree 2 under the + // scaling and the loop never produced a valid Q2). + // + // p1x_lab is the 3-momentum MAGNITUDE P1 = |p1_lab| (not an x-component). + // The naive expressions + // pqx = (m1^2 + m3^2 + 2 P1^2 + Q2 + 2 E1^2 (y-1)) / (2 P1) + // momq^2 = m1^2 + P1^2 + Q2 + E1^2 (y^2 - 1) + // lose precision because both carry dominant ~E1^2 terms that nearly cancel + // in pqy^2 = momq^2 - pqx^2. Substituting P1^2 = E1^2 - m1^2 cancels those + // terms analytically before the numeric evaluation: + // pqx = (m3^2 - m1^2 + Q2 + 2 E1^2 y) / (2 P1) + // momq^2 = Q2 + E1^2 y^2 double p1x_lab = std::sqrt(p1_lab.px() * p1_lab.px() + p1_lab.py() * p1_lab.py() + p1_lab.pz() * p1_lab.pz()); - double pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); - double momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); - double pqy_lab = std::numeric_limits::quiet_NaN(); - double Eq_lab; - - if (pqx_lab>momq_lab){ - // if current setting does not work, start looping through scalings - int maxIterations = 15; - int iteration = 0; - double p1_lab_x = p1_lab.px(); - double p1_lab_y = p1_lab.py(); - double p1_lab_z = p1_lab.pz(); - // loop to resolve precision issue - while (iteration <= maxIterations) { - Q2 = slowRescalingQ2(final_xi, final_y, E1_lab, target_mass_, - siren::utilities::Constants::charmMass); - p1x_lab = std::sqrt(p1_lab_x * p1_lab_x + p1_lab_y * p1_lab_y + p1_lab_z * p1_lab_z); - pqx_lab = (m1*m1 + m3*m3 + 2 * p1x_lab * p1x_lab + Q2 + 2 * E1_lab * E1_lab * (final_y - 1)) / (2.0 * p1x_lab); - momq_lab = std::sqrt(m1*m1 + p1x_lab*p1x_lab + Q2 + E1_lab * E1_lab * (final_y * final_y - 1)); - if (pqx_lab>momq_lab){ - //scale down - E1_lab /= 10; - p1_lab_x /= 10; - p1_lab_y /= 10; - p1_lab_z /= 10; - m1 /= 10; - m3 /= 10; - //iteration += 1 to scale back - iteration += 1; - continue; - } - pqy_lab = std::sqrt((momq_lab + pqx_lab) * (momq_lab - pqx_lab)); - break; - } - // //scale back - if (iteration > 0) { - E1_lab *= pow(10.0, iteration); - p1_lab_x *= pow(10.0, iteration); - p1_lab_y *= pow(10.0, iteration); - p1_lab_z *= pow(10.0, iteration); - m1 *= pow(10.0, iteration); - m3 *= pow(10.0, iteration); + double pqx_lab = (m3 * m3 - m1 * m1 + Q2 + 2.0 * E1_lab * E1_lab * final_y) / (2.0 * p1x_lab); + double momq2_lab = Q2 + E1_lab * E1_lab * final_y * final_y; + double pqy2_lab = momq2_lab - pqx_lab * pqx_lab; + // Clamp tiny-negative pqy^2 from edge roundoff to 0; genuinely-forbidden + // points (pqy^2 < 0 in exact arithmetic) are already excluded upstream by + // kinematicallyAllowed, so a large-magnitude negative here signals a + // sampler/predicate inconsistency and is a hard error. + if (pqy2_lab < 0.0) { + if (pqy2_lab > -1e-9 * std::max(momq2_lab, 1.0)) { + pqy2_lab = 0.0; + } else { + throw(siren::utilities::InjectionFailure( + "QuarkDISFromSpline::SampleFinalState: pqy^2 < 0 for a " + "kinematicallyAllowed point (sampler/predicate inconsistency)")); } - } else {pqy_lab = std::sqrt(momq_lab*momq_lab - pqx_lab *pqx_lab);} - if (std::isnan(pqy_lab)) { - throw(siren::utilities::InjectionFailure( - "QuarkDISFromSpline::SampleFinalState: precision loop failed to converge; pqy_lab is NaN")); } - Eq_lab = E1_lab * final_y; + double pqy_lab = std::sqrt(pqy2_lab); + double Eq_lab = E1_lab * final_y; geom3::UnitVector3 x_dir = geom3::UnitVector3::xAxis(); geom3::Vector3 p1_mom = p1_lab.momentum(); @@ -844,9 +863,9 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR if (xi >= 1.0) { throw(siren::utilities::InjectionFailure("xi >= 1.0; sampled slow-rescaling xi past unity")); } - rk::P4 p_parton(geom3::Vector3(0, 0, 0), xi * target_mass_); // parton at rest: (ξM, 0, 0, 0) - rk::P4 p4_lab = p_parton + pq_lab; // struck charm = ξ*p2 + q - rk::P4 p_spectator((1.0 - xi) * target_mass_, geom3::Vector3(0, 0, 0)); // spectator: ((1-ξ)M, 0,0, 0) + rk::P4 p_parton(geom3::Vector3(0, 0, 0), xi * target_mass_); // parton at rest: (xi*M, 0, 0, 0) + rk::P4 p4_lab = p_parton + pq_lab; // struck charm = xi*p2 + q + rk::P4 p_spectator((1.0 - xi) * target_mass_, geom3::Vector3(0, 0, 0)); // spectator: ((1-xi)*M, 0,0, 0) rk::P4 p3; rk::P4 p4; @@ -873,7 +892,18 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR p4X = p_spectator + p4_lab - p4CH; } while (p4X.dot(p4X) < 0); - // Save final state kinematics + // Save final state kinematics. + // + // NOTE (T3): the sampled momenta are written into the record's + // SecondaryParticleRecord vector (record.GetSecondaryParticleRecords()), NOT + // directly into an InteractionRecord's secondary_momenta. To obtain a finalized + // InteractionRecord with populated secondary_momenta, the caller must run + // CrossSectionDistributionRecord::Finalize (pybind: cdr.finalize(ir)) into an + // output record whose signature is set. Building an InteractionRecord by hand + // with empty/zero secondary_momenta and feeding it back to + // DifferentialCrossSection makes the primary-momentum Q2 path compute Q2 <= 0, + // forcing the stored-(xi,y) fallback branch; that is a caller mistake, not a + // limitation of SampleFinalState, which populates the state correctly here. std::vector & secondaries = record.GetSecondaryParticleRecords(); siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[lepton_index]; siren::dataclasses::SecondaryParticleRecord & hadron = secondaries[hadron_index]; @@ -993,16 +1023,38 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR } double QuarkDISFromSpline::FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const { + // D0:D+/-:Ds = 0.60:0.23:0.15, renormalized to sum to 1.0. The Lambda_c + // channel (~0.02) is not modeled, so its fraction is redistributed into the + // implemented D species; otherwise summing TotalCrossSection over the three + // registered D signatures would recover only 0.98 * sigma_inclusive and + // under-count the charm rate by ~2%. if (secondary == siren::dataclasses::Particle::ParticleType::D0 || secondary == siren::dataclasses::Particle::ParticleType::D0Bar) { - return 0.6; + return 0.6 / 0.98; } else if (secondary == siren::dataclasses::Particle::ParticleType::DPlus || secondary == siren::dataclasses::Particle::ParticleType::DMinus) { - return 0.23; + return 0.23 / 0.98; } else if (secondary == siren::dataclasses::Particle::ParticleType::DsPlus || secondary == siren::dataclasses::Particle::ParticleType::DsMinus) { - return 0.15; - } // Lambda_c not yet implemented + return 0.15 / 0.98; + } return 0; } +// UNBIASED-ONLY CONTRACT: SampleFinalState samples (xi,y) AND an independent +// fragmentation z (the inverse-CDF draw) and uniform azimuth phi that set the +// D-meson momentum. FinalStateProbability / DifferentialCrossSection account for +// (xi,y) only; the z and phi factors are NOT included here. They cancel exactly +// in the weight ratio ONLY when the same cross-section object provides both the +// injection and physical densities and no biased phase-space channel is installed +// on the D kinematics. Biasing the D kinematics is NOT supported and would produce +// incorrect weights. +// +// NORMALIZATION CONTRACT (P8): FinalStateProbability = dxs/txs is a normalized +// kinematic density ONLY if the external 1-D total-xs spline (txs) equals the +// integral of the differential spline (dxs) over the SAME truncated slow-rescaling +// domain: xi in [xiMin(E),1], y in [yMin(E),yMax(E)] with identical charm-threshold +// (Q2 = 2 M E xi y - m_c^2 > 0), W2 > (M + M_D0)^2, Q2 >= minimum_Q2_ cuts and the +// same TARGETMASS. If the upstream total spline integrates a different domain the +// density is mis-normalized. The fragmentation fraction is applied inside +// TotalCrossSection(record) so per-species txs carries the D-species branching. double QuarkDISFromSpline::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { // first compute the differential and total cross section double dxs = DifferentialCrossSection(interaction); @@ -1040,6 +1092,10 @@ std::vector QuarkDISFromSpline::GetPossibleSi } } +// UNBIASED-ONLY CONTRACT: the density covers only (xi,y); the independently-sampled +// fragmentation z and azimuth phi (set in SampleFinalState) are omitted. They cancel +// in the weight ratio only in the unbiased configuration (same cross-section object on +// both sides, no biased D-kinematics channel). Biasing D kinematics is NOT supported. std::vector QuarkDISFromSpline::DensityVariables() const { return std::vector{"Bjorken xi", "Bjorken y"}; } diff --git a/projects/interactions/private/test/CharmDISClosure_TEST.cxx b/projects/interactions/private/test/CharmDISClosure_TEST.cxx index ace3ea617..0c11c461a 100644 --- a/projects/interactions/private/test/CharmDISClosure_TEST.cxx +++ b/projects/interactions/private/test/CharmDISClosure_TEST.cxx @@ -50,10 +50,13 @@ class MockCharmXS : public CrossSection { bool override_afs; MockCharmXS(bool ff, bool ovr) : apply_ff(ff), override_afs(ovr) {} + // D0:D+/-:Ds = 0.60:0.23:0.15 renormalized to sum to 1.0 (Lambda_c not + // modeled, its fraction redistributed). Mirrors + // QuarkDISFromSpline::FragmentationFraction. static double FragmentationFraction(ParticleType d) { - if(d==ParticleType::D0 || d==ParticleType::D0Bar) return 0.6; - if(d==ParticleType::DPlus || d==ParticleType::DMinus) return 0.23; - if(d==ParticleType::DsPlus || d==ParticleType::DsMinus) return 0.15; + if(d==ParticleType::D0 || d==ParticleType::D0Bar) return 0.6 / 0.98; + if(d==ParticleType::DPlus || d==ParticleType::DMinus) return 0.23 / 0.98; + if(d==ParticleType::DsPlus || d==ParticleType::DsMinus) return 0.15 / 0.98; return 0.0; } // Stand-in for the 1-D inclusive charm total spline (independent of meson type). @@ -161,21 +164,32 @@ TEST(CharmDISClosure, OverrideRemovedClosesButOvercountsThreeX) { } } -// The proposed fix (override removed + fragmentation fraction in TotalCrossSection): -// the two sides agree AND both equal sigma*(0.6+0.23+0.15) = 0.98*sigma -- the -// inclusive charm cross section, partitioned not triple-counted. +// The fix (override removed + fragmentation fraction in TotalCrossSection, with +// the FFs renormalized to sum to 1.0): the two sides agree AND both equal the +// full inclusive charm cross section sigma -- partitioned across the three D +// species, not triple-counted and not the 2%-deficient 0.98*sigma. TEST(CharmDISClosure, FragmentationFractionRestoresPhysicalNormalization) { MockCharmXS xs(/*ff=*/true, /*override=*/false); - const double ff_sum = 0.6 + 0.23 + 0.15; + // FF sum = (0.6 + 0.23 + 0.15) / 0.98 == 1.0 (renormalized), so the + // partitioned total recovers the full inclusive sigma exactly. for(double E : {10.0, 100.0, 1000.0}) { double gen = gen_path(xs, ParticleType::NuMu, ParticleType::PPlus, E); double phys = phys_path(xs, ParticleType::NuMu, ParticleType::PPlus, E); double s = MockCharmXS::sigma_inclusive(E); EXPECT_NEAR(gen, phys, 1e-50); // closure ok - EXPECT_NEAR(gen, ff_sum * s, 1e-48); // and physically normalized (~sigma) + EXPECT_NEAR(gen, s, 1e-48); // and physically normalized (== inclusive sigma) } } +// The three implemented fragmentation fractions must partition the inclusive +// charm cross section exactly: they sum to 1.0 (Lambda_c fraction redistributed). +TEST(CharmDISClosure, FragmentationFractionsSumToOne) { + double sum = MockCharmXS::FragmentationFraction(ParticleType::D0) + + MockCharmXS::FragmentationFraction(ParticleType::DPlus) + + MockCharmXS::FragmentationFraction(ParticleType::DsPlus); + EXPECT_NEAR(sum, 1.0, 1e-12); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h index d4cf9dd45..45f6e46dc 100644 --- a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h +++ b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h @@ -41,6 +41,19 @@ namespace interactions { /// Charm-quark mass m_c=1.280 GeV (Constants::charmMass) and lightest charm /// hadron M_D0=1.86484 GeV (Constants::D0Mass) are taken from /// siren::utilities::Constants and are not configurable per-instance. +/// +/// UNBIASED-ONLY CONTRACT: SampleFinalState samples (xi,y) plus an independent +/// fragmentation z and uniform azimuth phi that set the D-meson momentum, but +/// DensityVariables/FinalStateProbability/DifferentialCrossSection account for +/// (xi,y) only. The omitted z/phi factors cancel in the weight ratio ONLY when +/// the same cross-section object supplies both the injection and physical +/// densities and no biased phase-space channel is installed on the D kinematics. +/// Biasing the D kinematics is NOT supported and will produce incorrect weights. +/// +/// NORMALIZATION CONTRACT: FinalStateProbability = dxs/txs is a normalized +/// kinematic density only if the external 1-D total-xs spline integrates the +/// SAME truncated slow-rescaling (xi,y) domain (and the same charm-threshold, +/// W2, Q2>=minimum_Q2_ cuts and TARGETMASS) as the differential spline. class QuarkDISFromSpline : public CrossSection { friend cereal::access; private: From 4b2ce817783d41e25e76d8e0b5e957ea836cc4f4 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 10:59:07 -0500 Subject: [PATCH 68/93] CharmMesonDecay/3Body: closure-correct FinalStateProbability, exclusive BRs, robust guards FinalStateProbability returns the normalized q^2 density SampleFinalState produces -- phase space times the angle-averaged V-A over the K/K* mixture with the sampler's accept-reject clipping -- so Sample == Density. The K-l-nu signatures use exclusive K and K* semileptonic branching ratios with the remainder in the hadronic catch-all, and unmatched signatures throw. --- .../interactions/private/CharmMesonDecay.cxx | 262 +++++++++++++-- .../private/CharmMesonDecay3Body.cxx | 195 ++++++++++- .../private/test/CharmMesonDecay_TEST.cxx | 302 +++++++++++------- .../SIREN/interactions/CharmMesonDecay.h | 10 +- .../SIREN/interactions/CharmMesonDecay3Body.h | 12 +- 5 files changed, 615 insertions(+), 166 deletions(-) diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx index 153e6453e..e6fbdb8a6 100644 --- a/projects/interactions/private/CharmMesonDecay.cxx +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -1,6 +1,7 @@ #include "SIREN/interactions/CharmMesonDecay.h" #include +#include #include #include @@ -121,9 +122,9 @@ double CharmMesonDecay::particleMass(siren::dataclasses::ParticleType particle) case siren::dataclasses::ParticleType::TauMinus: return( siren::utilities::Constants::tauMass ); case siren::dataclasses::ParticleType::DsPlus: - return 1.96834; // GeV (PDG 2022); not in Constants.h + return( siren::utilities::Constants::DsPlusMass ); case siren::dataclasses::ParticleType::DsMinus: - return 1.96834; + return( siren::utilities::Constants::DsMinusMass ); default: return(0.0); } @@ -149,8 +150,10 @@ double CharmMesonDecay::TotalDecayWidth(siren::dataclasses::Particle::ParticleTy // current problem: in implementation we see kaons and pions both as hadrons, but they should have different branching ratios and form factors double CharmMesonDecay::TotalDecayWidthForFinalState(dataclasses::InteractionRecord const & record) const { - double branching_ratio; - double tau; // total lifetime for all visible and invisible modes + // Sentinel-init: all valid branching ratios and lifetimes are strictly + // positive, so a negative value unambiguously flags an unmatched mode. + double branching_ratio = -1.0; + double tau = -1.0; // total lifetime for all visible and invisible modes // read in the signature and types siren::dataclasses::Particle::ParticleType primary = record.signature.primary_type; std::vector secondaries_vector = record.signature.secondary_types; @@ -176,7 +179,7 @@ double CharmMesonDecay::TotalDecayWidthForFinalState(dataclasses::InteractionRec siren::dataclasses::Particle::ParticleType::EPlus, siren::dataclasses::Particle::ParticleType::NuE}; std::set hadrons = {siren::dataclasses::Particle::ParticleType::Hadrons}; - // Anti-flavor (c̄) decay-mode sets, sign-conjugated from the c modes above. + // Anti-flavor (cbar) decay-mode sets, sign-conjugated from the c modes above. std::set k0_eminus_nuebar = {siren::dataclasses::Particle::ParticleType::K0, siren::dataclasses::Particle::ParticleType::EMinus, siren::dataclasses::Particle::ParticleType::NuEBar}; @@ -197,30 +200,32 @@ double CharmMesonDecay::TotalDecayWidthForFinalState(dataclasses::InteractionRec siren::dataclasses::Particle::ParticleType::NuEBar}; if (primary == siren::dataclasses::Particle::ParticleType::DPlus) { tau = 1040 * (1e-15); - // Physical PDG BRs for D+ semileptonic decay (2022 values). - // Pavel's downstream dimuon analysis uses a force-muonic variant - // (BR_mu = 1.0, BR_e = 0, BR_hadrons = 0); that lives on his fork. - if (secondaries == k0_eplus_nue) {branching_ratio = .1607;} - else if (secondaries == k0_muplus_numu) {branching_ratio = .176;} - else if (secondaries == hadrons) {branching_ratio = (1 - .1607 - .176);} + // Exclusive K + K* semileptonic BR (PDG): K0bar l nu = 8.74%, + // K*0bar l nu = 5.33%, summed to 14.07% because the K l nu signature + // is sampled with kinematic K/K* mixing (see SampleFinalState fracK). + // e and mu modes share the same value (lepton-mass effects sub-percent). + if (secondaries == k0_eplus_nue) {branching_ratio = .1407;} + else if (secondaries == k0_muplus_numu) {branching_ratio = .1407;} + else if (secondaries == hadrons) {branching_ratio = (1 - 2 * .1407);} } else if (primary == siren::dataclasses::Particle::ParticleType::DMinus) { // CP-mirror of D+ (same lifetime, same physical PDG BRs). tau = 1040 * (1e-15); - if (secondaries == k0_eminus_nuebar) {branching_ratio = .1607;} - else if (secondaries == k0_muminus_numubar) {branching_ratio = .176;} - else if (secondaries == hadrons) {branching_ratio = (1 - .1607 - .176);} + if (secondaries == k0_eminus_nuebar) {branching_ratio = .1407;} + else if (secondaries == k0_muminus_numubar) {branching_ratio = .1407;} + else if (secondaries == hadrons) {branching_ratio = (1 - 2 * .1407);} } else if (primary == siren::dataclasses::Particle::ParticleType::D0) { tau = 410.1 * (1e-15); - // Physical PDG BRs for D0 semileptonic decay (2022 values). - if (secondaries == kminus_eplus_nue) {branching_ratio = .0649;} - else if (secondaries == kminus_muplus_numu) {branching_ratio = .067;} - else if (secondaries == hadrons) {branching_ratio = (1 - .0649 - .067);} + // Exclusive K + K* semileptonic BR (PDG): K- l nu = 3.41%, + // K*- l nu = 2.17%, summed to 5.58% (kinematic K/K* mixing, see fracK). + if (secondaries == kminus_eplus_nue) {branching_ratio = .0558;} + else if (secondaries == kminus_muplus_numu) {branching_ratio = .0558;} + else if (secondaries == hadrons) {branching_ratio = (1 - 2 * .0558);} } else if (primary == siren::dataclasses::Particle::ParticleType::D0Bar) { // CP-mirror of D0 (same lifetime, same physical PDG BRs). tau = 410.1 * (1e-15); - if (secondaries == kplus_eminus_nuebar) {branching_ratio = .0649;} - else if (secondaries == kplus_muminus_numubar) {branching_ratio = .067;} - else if (secondaries == hadrons) {branching_ratio = (1 - .0649 - .067);} + if (secondaries == kplus_eminus_nuebar) {branching_ratio = .0558;} + else if (secondaries == kplus_muminus_numubar) {branching_ratio = .0558;} + else if (secondaries == hadrons) {branching_ratio = (1 - 2 * .0558);} } else if (primary == siren::dataclasses::Particle::ParticleType::DsPlus) { tau = 504 * (1e-15); // Ds+ lifetime: 504 fs (PDG) // Physical PDG BRs for Ds+ semileptonic decay. Inclusive @@ -238,7 +243,15 @@ double CharmMesonDecay::TotalDecayWidthForFinalState(dataclasses::InteractionRec else if (secondaries == hadrons) {branching_ratio = (1 - 2 * .0654);} } else { - std::cout << "this decay mode is not yet implemented!" << std::endl; + // Signatures are produced by GetPossibleSignaturesFromParent, so an + // unsupported primary here means the two lists are out of sync. Fail + // loudly rather than return an indeterminate width that would silently + // corrupt TotalDecayWidth / FinalStateProbability. + throw std::runtime_error("CharmMesonDecay::TotalDecayWidthForFinalState: unsupported primary particle type"); + } + // Guard the matched-primary / unmatched-secondaries case (sentinel still set). + if (tau <= 0.0 || branching_ratio < 0.0) { + throw std::runtime_error("CharmMesonDecay::TotalDecayWidthForFinalState: no implemented decay mode matches this signature"); } return branching_ratio * siren::utilities::Constants::hbar / tau * siren::utilities::Constants::GeV; } @@ -280,7 +293,7 @@ std::vector CharmMesonDecay::GetPossibleSigna hadron_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; signatures.push_back(hadron_signature); } else if (primary==siren::dataclasses::Particle::ParticleType::DMinus) { - // CP-mirror of D+: K0 (s̄d → contains s̄), e⁻/μ⁻, ν̄_e/ν̄_μ. + // CP-mirror of D+: K0 (sbar d -> contains sbar), e-/mu-, nubar_e/nubar_mu. semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::K0; semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::EMinus; semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuEBar; @@ -304,7 +317,7 @@ std::vector CharmMesonDecay::GetPossibleSigna hadron_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::Hadrons; signatures.push_back(hadron_signature); } else if (primary==siren::dataclasses::Particle::ParticleType::D0Bar) { - // CP-mirror of D0: K+ (us̄), e⁻/μ⁻, ν̄_e/ν̄_μ. + // CP-mirror of D0: K+ (u sbar), e-/mu-, nubar_e/nubar_mu. semilep_signature.secondary_types[0] = siren::dataclasses::Particle::ParticleType::KPlus; semilep_signature.secondary_types[1] = siren::dataclasses::Particle::ParticleType::EMinus; semilep_signature.secondary_types[2] = siren::dataclasses::Particle::ParticleType::NuEBar; @@ -353,7 +366,7 @@ std::vector CharmMesonDecay::FormFactorFromRecord(dataclasses::CrossSect std::vector constants; constants.resize(3); // check the primary and secondaries of the signature - // Form-factor constants are CP-symmetric — anti-flavor cases mirror the c cases. + // Form-factor constants are CP-symmetric -- anti-flavor cases mirror the c cases. if ((signature.primary_type == dataclasses::Particle::ParticleType::DPlus && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::K0Bar) || (signature.primary_type == dataclasses::Particle::ParticleType::DMinus && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::K0)) { constants[0] = 0.725; // this is f^+(0)|V_cs| for charged D @@ -527,7 +540,7 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco // Cumulative: eta=0.46, eta+eta'=0.62, all=1.0 double mEta = siren::utilities::Constants::EtaMass; // 0.547862 GeV double mEtaPrime = siren::utilities::Constants::EtaPrimeMass; // 0.95778 GeV - double mPhi = 1.01946; // GeV (PDG); not in Constants.h + double mPhi = siren::utilities::Constants::PhiMass; // 1.019461 GeV double r = random->Uniform(0, 1); if (r < 2.3 / 5.0) { mK = mEta; @@ -539,7 +552,10 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco } else { // D+/D0: K vs K*(892) with V-A weighting double mK_base = particleMass(record.signature.secondary_types[0]); - double mKstar = 0.89166; // K*(892) mass in GeV + // K*(892) mass; read from Constants so the FinalStateProbability + // normalizer (KStarMass()) and the sampler use the identical value + // (required for weighting closure). + double mKstar = KStarMass(); double fracK; if (primary == siren::dataclasses::Particle::ParticleType::DPlus || primary == siren::dataclasses::Particle::ParticleType::DMinus) { @@ -576,7 +592,7 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco double wtPSmax = 0.5 * p1Max * p23Max; // V-A matrix element upper bound (from Pythia, meMode == 22). - // For Ds we skip V-A weighting and use pure 3-body phase space — set + // For Ds we skip V-A weighting and use pure 3-body phase space -- set // wtMEmax = 1.0 and wtME = 1.0 inside the loop so the rejection test // (wtME < rand * wtMEmax) is always false and we exit after one iteration. double wtMEmax = is_Ds @@ -634,7 +650,7 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco p4l_Drest = p4l_m23rest.boost(boost_m23_to_Drest); p4nu_Drest = p4nu_m23rest.boost(boost_m23_to_Drest); - // --- Step 5: V-A matrix element weight (skipped for Ds — pure phase space) --- + // --- Step 5: V-A matrix element weight (skipped for Ds -- pure phase space) --- // wtME = m_D * E_lepton * (p_neutrino . p_kaon) in D rest frame // This is Lorentz invariant up to the m_D * E_l factor which equals p_D . p_l if (!is_Ds) { @@ -672,12 +688,190 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco } +// --------------------------------------------------------------------------- +// Closure-correct FinalStateProbability machinery. +// +// FinalStateProbability MUST be the exact normalized q^2 density that +// SampleFinalState produces (the Weighter consumes it as the physical +// final-state density). The form-factor DifferentialDecayWidth above is a +// separate physical quantity that the SAMPLER never used, so it cannot be the +// closure density. The sampler draws m23 = sqrt(q^2) flat with accept-reject +// on the phase-space weight p1Abs*p23Abs, then (for D+/D0) accept-rejects on +// the V-A weight wtME = mD*E_l*(p_nu . p_K). The marginal density of m23 of +// accepted events is therefore proportional to +// p1Abs(m23) * p23Abs(m23) * _angle , +// and the density in q^2 carries the Jacobian dm23/dq^2 = 1/(2 m23). The +// helpers below reproduce exactly this density; both the per-event numerator +// and the normalization integral use the same SampledQ2Density, so +// Sample == Density by construction. +// --------------------------------------------------------------------------- + +double CharmMesonDecay::KStarMass() { + // Single source of truth for the K*(892) mass shared by the sampler and the + // FinalStateProbability normalizer. + return siren::utilities::Constants::KPrimePlusMass; +} + +double CharmMesonDecay::VAWeightAngleAverage(double mD, double mK, double ml, double m23) const { + // Numerically exact angle-average of the sampler's ACCEPTED V-A weight, + // max(0, min(wtME, wtMEmax)), over the lepton polar angle cosTheta in [-1,1]. + // wtME = mD * E_l * (p_nu . p_K) in the D rest frame, evaluated with the SAME + // rk::P4 boost operations SampleFinalState uses (kaon along +z, m23 system + // recoiling along -z), so the density reproduced here matches the sampler's + // accepted distribution exactly (weighting closure). wtMEmax mirrors the + // sampler's rejection ceiling so the (rare) saturated region is reproduced too. + // A deterministic Simpson rule replaces the previous closed form, which had a + // sign-region error in its quadratic-root branch. + double mnu = 0.0; + double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) + * (mD + mK - m23) * (mD - mK + m23)) / mD; + double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) + * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; + if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; + // Same matrix-element ceiling as SampleFinalState (meMode == 22). + double wtMEmax = std::min(std::pow(mD, 4) / 16.0, + mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); + // m23 system recoils along -z in the D rest frame; kaon sits along +z. + rk::P4 p4m23_Drest(geom3::Vector3(0.0, 0.0, -p1Abs), m23); + rk::Boost boost_m23_to_Drest = p4m23_Drest.labBoost(); + rk::P4 p4K_Drest(geom3::Vector3(0.0, 0.0, p1Abs), mK); + const int N = 400; // even -> composite Simpson + double h = 2.0 / N; + double sum = 0.0; + for (int i = 0; i <= N; ++i) { + double c = -1.0 + i * h; + double s = std::sqrt(std::max(0.0, 1.0 - c * c)); + geom3::Vector3 dir(s, 0.0, c); + rk::P4 p4l_m23rest(p23Abs * dir, ml); + rk::P4 p4nu_m23rest(-p23Abs * dir, mnu); + rk::P4 p4l_Drest = p4l_m23rest.boost(boost_m23_to_Drest); + rk::P4 p4nu_Drest = p4nu_m23rest.boost(boost_m23_to_Drest); + double w = mD * p4l_Drest.e() * p4nu_Drest.dot(p4K_Drest); + if (w < 0.0) w = 0.0; + if (w > wtMEmax) w = wtMEmax; + double wgt = (i == 0 || i == N) ? 1.0 : ((i % 2) ? 4.0 : 2.0); + sum += wgt * w; + } + double integral = sum * h / 3.0; // integral over c in [-1,1] + return 0.5 * integral; // average (c-measure has width 2) +} + +double CharmMesonDecay::SampledQ2Density(double mD, double mK, double ml, double q2, bool apply_va) const { + // Unnormalized sampler density in q^2 for a single hadron-mass component. + double m23 = std::sqrt(q2); + double m23Max = mD - mK; + if (m23 <= (ml) || m23 >= m23Max) return 0.0; + double mnu = 0.0; + double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) + * (mD + mK - m23) * (mD - mK + m23)) / mD; + double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) + * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; + if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; + // Phase-space weight WITH the sampler's rejection ceiling. SampleFinalState + // accept-rejects m23 against wtPSmax = 0.5*p1Max*p23Max, which p1Abs*p23Abs can + // exceed; in that regime the sampler saturates (always accepts), so the + // accepted density is flat at wtPSmax. Reproduce the clip for exact closure. + double m23Min = ml + mnu; + double p1Max = 0.5 * std::sqrt((mD - mK - m23Min) * (mD + mK + m23Min) + * (mD + mK - m23Min) * (mD - mK + m23Min)) / mD; + double p23Max = 0.5 * std::sqrt((m23Max - ml - mnu) * (m23Max + ml + mnu) + * (m23Max + ml - mnu) * (m23Max - ml + mnu)) / m23Max; + double wtPSmax = 0.5 * p1Max * p23Max; + double wtPS = p1Abs * p23Abs; + if (wtPS > wtPSmax) wtPS = wtPSmax; + // V-A angle-averaged weight (D+/D0) or 1 for pure phase space (Ds). + double me = apply_va ? VAWeightAngleAverage(mD, mK, ml, m23) : 1.0; + // Jacobian dm23/dq^2 = 1/(2 m23) converts the m23 density to a q^2 density. + return wtPS * me / (2.0 * m23); +} + +double CharmMesonDecay::SampledQ2Normalization(double mD, double mK, double ml, bool apply_va) const { + // Integral of SampledQ2Density over the kinematically allowed q^2 range, used + // to normalize each mixture component to a proper pdf. Cached per + // (mD, mK, ml, apply_va) so the per-event FinalStateProbability call does not + // re-integrate (the kinematics are fixed by the primary/daughter masses). + double key = ((mD * 1e3 + mK) * 1e3 + ml) * 2.0 + (apply_va ? 1.0 : 0.0); + long ikey = (long)std::llround(key * 1e3); + auto it = norm_cache.find(ikey); + if (it != norm_cache.end()) return it->second; + double q2min = ml * ml; + double q2max = (mD - mK) * (mD - mK); + std::function integrand = [&](double q2) -> double { + return SampledQ2Density(mD, mK, ml, q2, apply_va); + }; + double n = siren::utilities::rombergIntegrate(integrand, q2min, q2max); + norm_cache[ikey] = n; + return n; +} + double CharmMesonDecay::FinalStateProbability(dataclasses::InteractionRecord const & record) const { - double dd = DifferentialDecayWidth(record); - double td = TotalDecayWidthForFinalState(record); - if (dd == 0) return 0.; - else if (td == 0) return 0.; - else return dd/td; + dataclasses::InteractionSignature signature = record.signature; + // Fully hadronic catch-all: a single Hadrons daughter carries the full + // primary 4-momentum, so the final state is deterministic and its density is 1. + if (signature.secondary_types.size() == 1 && + signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::Hadrons) { + return 1.0; + } + + siren::dataclasses::Particle::ParticleType primary = signature.primary_type; + bool is_Ds = (primary == siren::dataclasses::Particle::ParticleType::DsPlus || + primary == siren::dataclasses::Particle::ParticleType::DsMinus); + bool apply_va = !is_Ds; // Ds is pure phase space (no V-A matrix element) + + // Reconstruct q^2 = (p_D - p_hadron)^2 exactly as SampleFinalState defines it. + double mD = record.primary_mass; + double ml = record.secondary_masses[1]; + double mK = record.secondary_masses[0]; // hadron mass actually sampled + rk::P4 pD(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), mD); + rk::P4 pK(geom3::Vector3(record.secondary_momenta[0][1], record.secondary_momenta[0][2], record.secondary_momenta[0][3]), mK); + double q2 = (pD - pK).dot(pD - pK); + + // Build the hadron-mass mixture (masses + population fractions) that the + // sampler uses, identify which component the recorded mK belongs to, and + // normalize each component separately so the mixture weights are the true + // population fractions. + std::vector masses; + std::vector fractions; + if (is_Ds) { + // Ds -> (eta / eta' / phi) l nu, fractions 0.46 / 0.16 / 0.38 (matches + // the cumulative thresholds 2.3/5.0, 3.1/5.0 used in SampleFinalState). + masses = {siren::utilities::Constants::EtaMass, + siren::utilities::Constants::EtaPrimeMass, + siren::utilities::Constants::PhiMass}; + fractions = {2.3 / 5.0, 0.8 / 5.0, 1.9 / 5.0}; + } else { + // D+/D0 -> K l nu with kinematic K/K*(892) mixing. fracK matches the + // value used in SampleFinalState (PDG K vs K* semileptonic BRs). + double mK_base = particleMass(signature.secondary_types[0]); + double mKstar = KStarMass(); + double fracK; + if (primary == siren::dataclasses::Particle::ParticleType::DPlus || + primary == siren::dataclasses::Particle::ParticleType::DMinus) { + fracK = 8.74 / (8.74 + 5.33); + } else { + fracK = 3.41 / (3.41 + 2.17); + } + masses = {mK_base, mKstar}; + fractions = {fracK, 1.0 - fracK}; + } + + // Identify the component matching the recorded hadron mass. + int comp = -1; + double best = 1e30; + for (size_t i = 0; i < masses.size(); ++i) { + double d = std::abs(mK - masses[i]); + if (d < best) { best = d; comp = (int)i; } + } + if (comp < 0) return 0.0; + + double g = SampledQ2Density(mD, masses[comp], ml, q2, apply_va); + if (g <= 0.0) return 0.0; + double norm = SampledQ2Normalization(mD, masses[comp], ml, apply_va); + if (norm <= 0.0) return 0.0; + + // FinalStateProbability = mixture_fraction * (component density / component norm), + // a true normalized q^2 pdf summing over the hadron-mass mixture. + return fractions[comp] * g / norm; } std::vector CharmMesonDecay::DensityVariables() const { diff --git a/projects/interactions/private/CharmMesonDecay3Body.cxx b/projects/interactions/private/CharmMesonDecay3Body.cxx index 0672d205d..b5e0b53ae 100644 --- a/projects/interactions/private/CharmMesonDecay3Body.cxx +++ b/projects/interactions/private/CharmMesonDecay3Body.cxx @@ -1,6 +1,7 @@ #include "SIREN/interactions/CharmMesonDecay3Body.h" #include +#include #include #include @@ -133,8 +134,10 @@ double CharmMesonDecay3Body::TotalDecayWidth(siren::dataclasses::Particle::Parti // current problem: in implementation we see kaons and pions both as hadrons, but they should have different branching ratios and form factors double CharmMesonDecay3Body::TotalDecayWidthForFinalState(dataclasses::InteractionRecord const & record) const { - double branching_ratio; - double tau; // total lifetime for all visible and invisible modes + // Sentinel-init: all valid branching ratios and lifetimes are strictly + // positive, so a negative value unambiguously flags an unmatched mode. + double branching_ratio = -1.0; + double tau = -1.0; // total lifetime for all visible and invisible modes // read in the signature and types siren::dataclasses::Particle::ParticleType primary = record.signature.primary_type; std::vector secondaries_vector = record.signature.secondary_types; @@ -156,17 +159,29 @@ double CharmMesonDecay3Body::TotalDecayWidthForFinalState(dataclasses::Interacti std::set hadrons = {siren::dataclasses::Particle::ParticleType::Hadrons}; if (primary == siren::dataclasses::Particle::ParticleType::DPlus) { tau = 1040 * (1e-15); - if (secondaries == k0_eplus_nue) {branching_ratio = .1607;} // e+ semileptonic mode according to pdg - else if (secondaries == k0_muplus_numu) {branching_ratio = .176;} // mu+ anything according to pdg - else if (secondaries == hadrons) {branching_ratio = (1 - .1607 - .176);} // everything else + // Exclusive K + K* semileptonic BR (PDG): K0bar l nu = 8.74%, + // K*0bar l nu = 5.33%, summed to 14.07% (kinematic K/K* mixing, see fracK). + // e and mu modes share the same value (lepton-mass effects sub-percent). + if (secondaries == k0_eplus_nue) {branching_ratio = .1407;} + else if (secondaries == k0_muplus_numu) {branching_ratio = .1407;} + else if (secondaries == hadrons) {branching_ratio = (1 - 2 * .1407);} // everything else } else if (primary == siren::dataclasses::Particle::ParticleType::D0) { tau = 410.1 * (1e-15); - if (secondaries == kminus_eplus_nue) {branching_ratio = .0649;} // e+ semileptonic mode according to pdg - else if (secondaries == kminus_muplus_numu) {branching_ratio = .067;} // mu+ anything according to pdg - else if (secondaries == hadrons) {branching_ratio = (1 - .0649 - .067);} // everything else + // Exclusive K + K* semileptonic BR (PDG): K- l nu = 3.41%, + // K*- l nu = 2.17%, summed to 5.58% (kinematic K/K* mixing, see fracK). + if (secondaries == kminus_eplus_nue) {branching_ratio = .0558;} + else if (secondaries == kminus_muplus_numu) {branching_ratio = .0558;} + else if (secondaries == hadrons) {branching_ratio = (1 - 2 * .0558);} // everything else } else { - std::cout << "this decay mode is not yet implemented!" << std::endl; + // Signatures come from GetPossibleSignaturesFromParent, so an unsupported + // primary means the lists are out of sync. Fail loudly rather than + // return an indeterminate width. + throw std::runtime_error("CharmMesonDecay3Body::TotalDecayWidthForFinalState: unsupported primary particle type"); + } + // Guard the matched-primary / unmatched-secondaries case (sentinel still set). + if (tau <= 0.0 || branching_ratio < 0.0) { + throw std::runtime_error("CharmMesonDecay3Body::TotalDecayWidthForFinalState: no implemented decay mode matches this signature"); } return branching_ratio * siren::utilities::Constants::hbar / tau * siren::utilities::Constants::GeV; } @@ -353,7 +368,7 @@ void CharmMesonDecay3Body::SampleFinalStateHadronic(dataclasses::CrossSectionDis } void CharmMesonDecay3Body::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { - // Hadronic decay branch — identical to the 2-body class. + // Hadronic decay branch -- identical to the 2-body class. dataclasses::InteractionSignature signature = record.signature; if (signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::Hadrons) { SampleFinalStateHadronic(record, random); @@ -378,7 +393,7 @@ void CharmMesonDecay3Body::SampleFinalState(dataclasses::CrossSectionDistributio // secondary particle's ParticleType is always left at the K species from // the signature (K0bar / K-), even when the K* mass was drawn. We do not // advertise separate K* signatures in GetPossibleSignaturesFromParent() - // and we do not re-type the secondary. This is intentional — for our use + // and we do not re-type the secondary. This is intentional -- for our use // case (weighting lepton/neutrino kinematics correctly in the presence of // the resonant K* contribution) the mass treatment is what matters, and // downstream propagation treats the secondary as a pseudoscalar K. @@ -390,7 +405,10 @@ void CharmMesonDecay3Body::SampleFinalState(dataclasses::CrossSectionDistributio double mnu = 0.0; // (massless) neutrino // K/K* mixing fractions from PDG semileptonic branching ratios - double mKstar = 0.89166; // K*(892) mass [GeV] + // K*(892) mass read from Constants so the sampler and the + // FinalStateProbability normalizer (KStarMass()) use the identical value + // (required for weighting closure). + double mKstar = KStarMass(); double fracK; if (record.signature.primary_type == siren::dataclasses::Particle::ParticleType::DPlus) { // D+ -> Kbar0 l nu: BR_K = 8.74%, BR_K*bar = 5.33% @@ -496,12 +514,155 @@ void CharmMesonDecay3Body::SampleFinalState(dataclasses::CrossSectionDistributio neutrino.SetHelicity(record.primary_helicity); } +// --------------------------------------------------------------------------- +// Closure-correct FinalStateProbability machinery (see CharmMesonDecay.cxx for +// the full derivation). FinalStateProbability MUST be the normalized q^2 +// density that SampleFinalState produces: the sampler draws m23 = sqrt(q^2) +// flat with accept-reject on p1Abs*p23Abs, then accept-rejects on the V-A +// weight wtME = mD*E_l*(p_nu . p_K). The accepted m23 density is therefore +// proportional to p1Abs*p23Abs*_angle, and the q^2 density carries +// the Jacobian 1/(2 m23). Both the numerator and the per-component normalizer +// use the same SampledQ2Density, so Sample == Density by construction. The +// form-factor DifferentialDecayWidth is a separate quantity the sampler never +// used and is NOT the closure density. +// --------------------------------------------------------------------------- + +double CharmMesonDecay3Body::KStarMass() { + // Single source of truth for the K*(892) mass shared by the sampler and the + // FinalStateProbability normalizer. + return siren::utilities::Constants::KPrimePlusMass; +} + +double CharmMesonDecay3Body::VAWeightAngleAverage(double mD, double mK, double ml, double m23) const { + // Numerically exact angle-average of the sampler's ACCEPTED V-A weight, + // max(0, min(wtME, wtMEmax)), over the lepton polar angle cosTheta in [-1,1]. + // wtME = mD * E_l * (p_nu . p_K) in the D rest frame, evaluated with the SAME + // rk::P4 boost operations SampleFinalState uses (kaon along +z, m23 system + // recoiling along -z), so the reproduced density matches the sampler exactly + // (closure). wtMEmax mirrors the sampler's rejection ceiling. A deterministic + // Simpson rule replaces the previous closed form, which had a sign-region + // error in its quadratic-root branch. + double mnu = 0.0; + double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) + * (mD + mK - m23) * (mD - mK + m23)) / mD; + double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) + * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; + if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; + // Same matrix-element ceiling as SampleFinalState (meMode == 22). + double wtMEmax = std::min(std::pow(mD, 4) / 16.0, + mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); + // m23 system recoils along -z in the D rest frame; kaon sits along +z. + rk::P4 p4m23_Drest(geom3::Vector3(0.0, 0.0, -p1Abs), m23); + rk::Boost boost_m23_to_Drest = p4m23_Drest.labBoost(); + rk::P4 p4K_Drest(geom3::Vector3(0.0, 0.0, p1Abs), mK); + const int N = 400; // even -> composite Simpson + double h = 2.0 / N; + double sum = 0.0; + for (int i = 0; i <= N; ++i) { + double c = -1.0 + i * h; + double s = std::sqrt(std::max(0.0, 1.0 - c * c)); + geom3::Vector3 dir(s, 0.0, c); + rk::P4 p4l_m23rest(p23Abs * dir, ml); + rk::P4 p4nu_m23rest(-p23Abs * dir, mnu); + rk::P4 p4l_Drest = p4l_m23rest.boost(boost_m23_to_Drest); + rk::P4 p4nu_Drest = p4nu_m23rest.boost(boost_m23_to_Drest); + double w = mD * p4l_Drest.e() * p4nu_Drest.dot(p4K_Drest); + if (w < 0.0) w = 0.0; + if (w > wtMEmax) w = wtMEmax; + double wgt = (i == 0 || i == N) ? 1.0 : ((i % 2) ? 4.0 : 2.0); + sum += wgt * w; + } + double integral = sum * h / 3.0; // integral over c in [-1,1] + return 0.5 * integral; // average (c-measure has width 2) +} + +double CharmMesonDecay3Body::SampledQ2Density(double mD, double mK, double ml, double q2, bool apply_va) const { + double m23 = std::sqrt(q2); + double m23Max = mD - mK; + if (m23 <= ml || m23 >= m23Max) return 0.0; + double mnu = 0.0; + double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) + * (mD + mK - m23) * (mD - mK + m23)) / mD; + double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) + * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; + if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; + // Phase-space weight WITH the sampler's rejection ceiling. SampleFinalState + // accept-rejects m23 against wtPSmax = 0.5*p1Max*p23Max, which p1Abs*p23Abs can + // exceed; there the sampler saturates (always accepts), so the accepted + // density is flat at wtPSmax. Reproduce the clip for exact closure. + double m23Min = ml + mnu; + double p1Max = 0.5 * std::sqrt((mD - mK - m23Min) * (mD + mK + m23Min) + * (mD + mK - m23Min) * (mD - mK + m23Min)) / mD; + double p23Max = 0.5 * std::sqrt((m23Max - ml - mnu) * (m23Max + ml + mnu) + * (m23Max + ml - mnu) * (m23Max - ml + mnu)) / m23Max; + double wtPSmax = 0.5 * p1Max * p23Max; + double wtPS = p1Abs * p23Abs; + if (wtPS > wtPSmax) wtPS = wtPSmax; + double me = apply_va ? VAWeightAngleAverage(mD, mK, ml, m23) : 1.0; + return wtPS * me / (2.0 * m23); +} + +double CharmMesonDecay3Body::SampledQ2Normalization(double mD, double mK, double ml, bool apply_va) const { + // Cached per (mD, mK, ml, apply_va) so per-event FinalStateProbability does + // not re-integrate the fixed-kinematics normalizer. + double key = ((mD * 1e3 + mK) * 1e3 + ml) * 2.0 + (apply_va ? 1.0 : 0.0); + long ikey = (long)std::llround(key * 1e3); + auto it = norm_cache.find(ikey); + if (it != norm_cache.end()) return it->second; + double q2min = ml * ml; + double q2max = (mD - mK) * (mD - mK); + std::function integrand = [&](double q2) -> double { + return SampledQ2Density(mD, mK, ml, q2, apply_va); + }; + double n = siren::utilities::rombergIntegrate(integrand, q2min, q2max); + norm_cache[ikey] = n; + return n; +} + double CharmMesonDecay3Body::FinalStateProbability(dataclasses::InteractionRecord const & record) const { - double dd = DifferentialDecayWidth(record); - double td = TotalDecayWidthForFinalState(record); - if (dd == 0) return 0.; - else if (td == 0) return 0.; - else return dd/td; + dataclasses::InteractionSignature signature = record.signature; + // Fully hadronic catch-all: deterministic final state, density 1. + if (signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::Hadrons) { + return 1.0; + } + + siren::dataclasses::Particle::ParticleType primary = signature.primary_type; + bool apply_va = true; // 3-body class always applies the V-A matrix element + + double mD = record.primary_mass; + double ml = record.secondary_masses[1]; + double mK = record.secondary_masses[0]; // hadron mass actually sampled + rk::P4 pD(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), mD); + rk::P4 pK(geom3::Vector3(record.secondary_momenta[0][1], record.secondary_momenta[0][2], record.secondary_momenta[0][3]), mK); + double q2 = (pD - pK).dot(pD - pK); + + // K/K*(892) kinematic mixture matching SampleFinalState's fracK. + double mK_base = particleMass(signature.secondary_types[0]); + double mKstar = KStarMass(); + double fracK; + if (primary == siren::dataclasses::Particle::ParticleType::DPlus) { + fracK = 8.74 / (8.74 + 5.33); + } else { + fracK = 3.41 / (3.41 + 2.17); + } + std::vector masses = {mK_base, mKstar}; + std::vector fractions = {fracK, 1.0 - fracK}; + + int comp = -1; + double best = 1e30; + for (size_t i = 0; i < masses.size(); ++i) { + double d = std::abs(mK - masses[i]); + if (d < best) { best = d; comp = (int)i; } + } + if (comp < 0) return 0.0; + + double g = SampledQ2Density(mD, masses[comp], ml, q2, apply_va); + if (g <= 0.0) return 0.0; + double norm = SampledQ2Normalization(mD, masses[comp], ml, apply_va); + if (norm <= 0.0) return 0.0; + + // Normalized q^2 pdf summing over the K/K* mixture. + return fractions[comp] * g / norm; } std::vector CharmMesonDecay3Body::DensityVariables() const { diff --git a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx index 808ec6c7a..76676cf46 100644 --- a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx +++ b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx @@ -1,10 +1,13 @@ /** - * Unit test for CharmMesonDecay CDF and Interpolator1D behavior. + * Unit test for CharmMesonDecay CDF / Interpolator1D behavior and the + * SampleFinalState <-> FinalStateProbability closure invariant. * * Tests: * 1. Interpolator1D with a known linear inverse CDF (sanity check) * 2. Interpolator1D with the actual D meson decay CDF table - * 3. CharmMesonDecay::SampleFinalState Q² distribution + * 2b. DifferentialDecayWidth (4-arg + from-record) vs analytic form factor + * 3. SampleFinalState q^2 distribution closes with FinalStateProbability + * 4. TotalDecayWidthForFinalState throws on unsupported signatures * * Build (from SIREN/build): * make -j4 (if registered in CMakeLists.txt) @@ -21,6 +24,7 @@ #include #include #include +#include #include @@ -37,7 +41,7 @@ using namespace siren::utilities; using namespace siren::interactions; using namespace siren::dataclasses; -// ── Test 1: Interpolator1D with known linear CDF ───────────────────────── +// --- Test 1: Interpolator1D with known linear CDF ------------------------ TEST(Interpolator1D, LinearInverseCDF) { // CDF: F(x) = x / 1.4, so inverse: x = 1.4 * u @@ -53,9 +57,6 @@ TEST(Interpolator1D, LinearInverseCDF) { Interpolator1D interp(table); - std::cout << "Test 1: Linear inverse CDF" << std::endl; - std::cout << " IsLog: " << interp.IsLog() << std::endl; - // Check known values double tol = 0.01; EXPECT_NEAR(interp(0.0), 0.0, tol); @@ -64,12 +65,6 @@ TEST(Interpolator1D, LinearInverseCDF) { EXPECT_NEAR(interp(0.75), 1.05, tol); EXPECT_NEAR(interp(1.0), 1.4, tol); - std::cout << " interp(0.0) = " << interp(0.0) << " (expect 0.0)" << std::endl; - std::cout << " interp(0.25) = " << interp(0.25) << " (expect 0.35)" << std::endl; - std::cout << " interp(0.5) = " << interp(0.5) << " (expect 0.7)" << std::endl; - std::cout << " interp(0.75) = " << interp(0.75) << " (expect 1.05)" << std::endl; - std::cout << " interp(1.0) = " << interp(1.0) << " (expect 1.4)" << std::endl; - // Sample mean should be 0.7 double sum = 0; int Nsamp = 10000; @@ -79,11 +74,10 @@ TEST(Interpolator1D, LinearInverseCDF) { sum += interp(u); } double mean = sum / Nsamp; - std::cout << " Mean from " << Nsamp << " samples: " << mean << " (expect ~0.7)" << std::endl; EXPECT_NEAR(mean, 0.7, 0.05); } -// ── Test 2: Interpolator1D with D meson decay CDF ─────────────────────── +// --- Test 2: Interpolator1D with D meson decay CDF ----------------------- TEST(Interpolator1D, DMesonDecayCDF) { // Replicate computeDiffGammaCDF for D0 -> K- e+ nu_e @@ -94,9 +88,6 @@ TEST(Interpolator1D, DMesonDecayCDF) { double ms = 2.00697; double GF = Constants::FermiConstant; - std::cout << "\nTest 2: D meson decay CDF" << std::endl; - std::cout << " mD = " << mD << ", mK = " << mK << std::endl; - // DifferentialDecayWidth (fixed version) auto dGamma = [&](double Q2) -> double { double Q2tilde = Q2 / (ms * ms); @@ -111,7 +102,6 @@ TEST(Interpolator1D, DMesonDecayCDF) { // Normalize (same as SIREN: Romberg integration over [0, 1.4]) std::function pdf_func = dGamma; double norm = rombergIntegrate(pdf_func, 0.0, 1.4); - std::cout << " Normalization: " << norm << std::endl; auto normed_pdf = [&](double Q2) -> double { return dGamma(Q2) / norm; @@ -151,46 +141,22 @@ TEST(Interpolator1D, DMesonDecayCDF) { cdf_vector.push_back(1.0); pdf_vector.push_back(0); - std::cout << " CDF table size: " << cdf_vector.size() << " nodes" << std::endl; - std::cout << " CDF last value before forced 1.0: " << cdf_vector[cdf_vector.size() - 2] << std::endl; - // Build Interpolator1D (inverse CDF: x=CDF, f=Q2) TableData1D inverse_cdf_data; inverse_cdf_data.x = cdf_vector; inverse_cdf_data.f = cdf_Q2_nodes; - Interpolator1D inverseCdf(inverse_cdf_data); - - std::cout << " Interpolator1D IsLog: " << inverseCdf.IsLog() << std::endl; - std::cout << " MinX: " << inverseCdf.MinX() << ", MaxX: " << inverseCdf.MaxX() << std::endl; - - // Evaluate at known CDF values - std::cout << "\n Inverse CDF spot checks:" << std::endl; - double test_cdfs[] = {0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}; - for (double u : test_cdfs) { - double q2 = inverseCdf(u); - // Also compute expected by linear interpolation - double q2_expected = 0; - for (size_t j = 0; j < cdf_vector.size() - 1; ++j) { - if (cdf_vector[j] <= u && u <= cdf_vector[j + 1]) { - double t = (cdf_vector[j + 1] - cdf_vector[j] > 0) ? - (u - cdf_vector[j]) / (cdf_vector[j + 1] - cdf_vector[j]) : 0; - q2_expected = cdf_Q2_nodes[j] + (cdf_Q2_nodes[j + 1] - cdf_Q2_nodes[j]) * t; - break; - } - } - std::cout << " CDF=" << u << " -> Q2=" << q2 << " (linear expected: " << q2_expected - << ", diff=" << q2 - q2_expected << ")" << std::endl; - } + Interpolator1D inverse_cdf(inverse_cdf_data); - // Sample and compute mean Q² + // Sample and compute mean Q^2 from the interpolator and from a manual + // linear interpolation; the two must agree (this guards the interpolator). SIREN_random rng; int Nsamp = 100000; double sum_q2 = 0; double sum_q2_linear = 0; for (int i = 0; i < Nsamp; ++i) { double u = rng.Uniform(0, 1); - sum_q2 += inverseCdf(u); + sum_q2 += inverse_cdf(u); // Linear interpolation for comparison double q2_lin = 0; for (size_t j = 0; j < cdf_vector.size() - 1; ++j) { @@ -206,36 +172,23 @@ TEST(Interpolator1D, DMesonDecayCDF) { double mean_interp = sum_q2 / Nsamp; double mean_linear = sum_q2_linear / Nsamp; - std::cout << "\n Mean Q² from Interpolator1D: " << mean_interp << std::endl; - std::cout << " Mean Q² from linear interp: " << mean_linear << std::endl; - std::cout << " Expected (correct): ~0.58" << std::endl; - std::cout << " Difference: " << mean_interp - mean_linear << std::endl; - - // The test: Interpolator1D should give similar results to linear interpolation - // If not, this confirms the Interpolator1D bias - if (std::abs(mean_interp - mean_linear) > 0.05) { - std::cout << "\n *** INTERPOLATOR1D BIAS DETECTED ***" << std::endl; - std::cout << " Interpolator1D gives " << mean_interp << " vs linear " << mean_linear << std::endl; - } EXPECT_NEAR(mean_interp, mean_linear, 0.05); } -// ── Test 2b: Compare DifferentialDecayWidth with analytical formula ────── +// --- Test 2b: DifferentialDecayWidth vs analytic form factor ------------- TEST(CharmMesonDecay, DiffDecayWidthComparison) { - // Check if the SIREN DifferentialDecayWidth matches our analytical formula + // Verify the SIREN DifferentialDecayWidth matches the analytic single-pole + // form-factor formula, both via the 4-arg path and the from-record path. double mD = Constants::D0Mass; // PDG D0 mass double mK = Constants::KMinusMass; double F0CKM = 0.719; - double alpha = 0.50; + double alpha = 0.50; // D0 pole parameter double ms_star = 2.00697; double GF = Constants::FermiConstant; - std::cout << "\nTest 2b: DifferentialDecayWidth comparison" << std::endl; - std::cout << " Constants::D0Mass = " << Constants::D0Mass << " (PDG D0 = 1.86484, PDG D+ = 1.86966)" << std::endl; - std::cout << " Constants::DPlusMass = " << Constants::DPlusMass << std::endl; - - // Our analytical formula + // Analytic form factor. Note EK enters only as EK*EK, so the sign + // convention of EK is irrelevant (matches the source 4-arg routine). auto dGamma_analytical = [&](double Q2) -> double { double Q2tilde = Q2 / (ms_star * ms_star); double ff2 = std::pow(F0CKM / ((1 - Q2tilde) * (1 - alpha * Q2tilde)), 2); @@ -245,19 +198,16 @@ TEST(CharmMesonDecay, DiffDecayWidthComparison) { return std::pow(GF, 2) / (24 * std::pow(M_PI, 3)) * ff2 * std::pow(std::sqrt(pk_sq), 3); }; - // Build an InteractionRecord to call the SIREN DifferentialDecayWidth CharmMesonDecay decay(ParticleType::D0); auto sigs = decay.GetPossibleSignaturesFromParent(ParticleType::D0); auto sig = sigs[0]; // D0 -> K- e+ nu_e - std::cout << "\n Q² | analytical | SIREN DDW | ratio" << std::endl; double Q2_tests[] = {0.01, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0, 1.2, 1.35}; for (double Q2 : Q2_tests) { double anal = dGamma_analytical(Q2); + if (anal <= 0.0) continue; - // For SIREN DDW, we need to construct an InteractionRecord with the right kinematics - // DDW(InteractionRecord) reconstructs Q² from 4-momenta, so we need to set them up - // at the given Q². Use D rest frame for simplicity. + // from-record path: reconstruct the kinematics in the D rest frame. double EK_rest = (mD * mD + mK * mK - Q2) / (2 * mD); double PK_rest = std::sqrt(std::max(0.0, EK_rest * EK_rest - mK * mK)); @@ -268,52 +218,105 @@ TEST(CharmMesonDecay, DiffDecayWidthComparison) { rec.target_mass = 0; rec.secondary_momenta = { {EK_rest, PK_rest, 0, 0}, // K along x - {0, 0, 0, 0}, // lepton (not used by DDW) - {0, 0, 0, 0} // neutrino (not used by DDW) + {0, 0, 0, 0}, // lepton (not used by DDW) + {0, 0, 0, 0} // neutrino (not used by DDW) }; rec.secondary_masses = {mK, Constants::electronMass, 0.0}; double siren_ddw_from_record = decay.DifferentialDecayWidth(rec); - // Also call 4-arg DDW directly with same constants + // 4-arg path with the D0 form-factor constants. std::vector my_constants = {F0CKM, alpha, ms_star}; double siren_ddw_4arg = decay.DifferentialDecayWidth(my_constants, Q2, mD, mK); - // And with alpha=0.44 (FormFactorFromRecord value) - std::vector ffr_constants = {0.719, 0.44, 2.00697}; - double siren_ddw_4arg_044 = decay.DifferentialDecayWidth(ffr_constants, Q2, mD, mK); - - std::cout << " Q2=" << Q2 - << " | anal=" << anal - << " | 4arg(a=0.50)=" << siren_ddw_4arg - << " | 4arg(a=0.44)=" << siren_ddw_4arg_044 - << " | fromRecord=" << siren_ddw_from_record - << " | ratioRec/anal=" << siren_ddw_from_record / anal - << std::endl; + // The 4-arg routine reproduces the analytic formula to floating point. + EXPECT_NEAR(siren_ddw_4arg, anal, std::abs(anal) * 1e-9); + // The from-record path round-trips Q^2 through 4-vectors; looser tol. + EXPECT_NEAR(siren_ddw_from_record, anal, std::abs(anal) * 1e-6); } + + // Pin the per-meson pole parameter alpha used by FormFactorFromRecord: + // 0.50 for D0, 0.44 for D+ (both intentional; not a bug to "correct"). + InteractionRecord d0rec; + d0rec.signature = sig; + d0rec.primary_mass = mD; + d0rec.primary_momentum = {mD, 0, 0, 0}; + d0rec.secondary_momenta = {{mK, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; + d0rec.secondary_masses = {mK, Constants::electronMass, 0.0}; + CrossSectionDistributionRecord d0cdr(d0rec); + std::vector d0ff = decay.FormFactorFromRecord(d0cdr); + EXPECT_NEAR(d0ff[1], 0.50, 1e-12); + + CharmMesonDecay decayp(ParticleType::DPlus); + auto sigsp = decayp.GetPossibleSignaturesFromParent(ParticleType::DPlus); + auto sigp = sigsp[0]; // D+ -> K0bar e+ nu + double mDp = Constants::DPlusMass; + double mK0 = Constants::K0Mass; + InteractionRecord dprec; + dprec.signature = sigp; + dprec.primary_mass = mDp; + dprec.primary_momentum = {mDp, 0, 0, 0}; + dprec.secondary_momenta = {{mK0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; + dprec.secondary_masses = {mK0, Constants::electronMass, 0.0}; + CrossSectionDistributionRecord dpcdr(dprec); + std::vector dpff = decayp.FormFactorFromRecord(dpcdr); + EXPECT_NEAR(dpff[1], 0.44, 1e-12); } -// ── Test 3: CharmMesonDecay SampleFinalState Q² ───────────────────────── +// --- Test 3: SampleFinalState q^2 closes with FinalStateProbability ------- + +namespace { +// Reconstruct q^2 = (p_D - p_K)^2 from a finalized record. +double reconstruct_q2(const InteractionRecord & rec) { + double qE = rec.primary_momentum[0] - rec.secondary_momenta[0][0]; + double qpx = rec.primary_momentum[1] - rec.secondary_momenta[0][1]; + double qpy = rec.primary_momentum[2] - rec.secondary_momenta[0][2]; + double qpz = rec.primary_momentum[3] - rec.secondary_momenta[0][3]; + return qE * qE - qpx * qpx - qpy * qpy - qpz * qpz; +} + +// Build a record at a given q^2 in the D rest frame with hadron mass mK so that +// FinalStateProbability can be evaluated on a single mixture component. +InteractionRecord make_record_at_q2(const InteractionSignature & sig, + double mD, double mK, double ml, double q2) { + double EK = (mD * mD + mK * mK - q2) / (2 * mD); + double PK = std::sqrt(std::max(0.0, EK * EK - mK * mK)); + InteractionRecord rec; + rec.signature = sig; + rec.primary_mass = mD; + rec.primary_momentum = {mD, 0, 0, 0}; + rec.target_mass = 0; + rec.secondary_momenta = {{EK, PK, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; + rec.secondary_masses = {mK, ml, 0.0}; + return rec; +} +} // namespace TEST(CharmMesonDecay, SampledQ2Distribution) { - // Create D0 decay + // D0 -> K- e+ nu_e with kinematic K/K*(892) mixing. CharmMesonDecay decay(ParticleType::D0); auto sigs = decay.GetPossibleSignaturesFromParent(ParticleType::D0); - // sig[0] = D0 -> K- e+ nu_e auto sig = sigs[0]; - std::cout << "\nTest 3: CharmMesonDecay sampled Q² distribution" << std::endl; - std::cout << " Signature: D0 -> "; - for (auto s : sig.secondary_types) std::cout << (int)s << " "; - std::cout << std::endl; - double mD = Constants::D0Mass; - double E_D = 100.0; // 100 GeV D meson + double ml = Constants::electronMass; + double mK = Constants::KMinusMass; + double mKstar = Constants::KPrimePlusMass; + + double E_D = 100.0; // boosted D meson (q^2 is frame-independent) double p_D = std::sqrt(E_D * E_D - mD * mD); auto rng = std::make_shared(); - int Nsamp = 5000; + int Nsamp = 30000; double sum_q2 = 0; + double sum_q2_sq = 0; + + // Histogram q^2 separately for the K and K* sub-populations. + const int NB = 16; + double q2lo = 0.0; + double q2hi = (mD - mK) * (mD - mK); // widest support (K mass) + double bw = (q2hi - q2lo) / NB; + std::vector countK(NB, 0), countKstar(NB, 0); for (int i = 0; i < Nsamp; ++i) { InteractionRecord rec; @@ -324,33 +327,108 @@ TEST(CharmMesonDecay, SampledQ2Distribution) { rec.target_mass = 0; rec.target_helicity = 0; rec.secondary_momenta = {{0,0,0,0}, {0,0,0,0}, {0,0,0,0}}; - rec.secondary_masses = {Constants::KMinusMass, Constants::electronMass, 0.0}; + rec.secondary_masses = {mK, ml, 0.0}; rec.secondary_helicities = {0, 0, 0}; CrossSectionDistributionRecord cdr(rec); decay.SampleFinalState(cdr, rng); cdr.Finalize(rec); - // Reconstruct Q² = (p_D - p_K)² - double qE = rec.primary_momentum[0] - rec.secondary_momenta[0][0]; - double qpx = rec.primary_momentum[1] - rec.secondary_momenta[0][1]; - double qpy = rec.primary_momentum[2] - rec.secondary_momenta[0][2]; - double qpz = rec.primary_momentum[3] - rec.secondary_momenta[0][3]; - double Q2 = qE * qE - qpx * qpx - qpy * qpy - qpz * qpz; - sum_q2 += Q2; + double q2 = reconstruct_q2(rec); + sum_q2 += q2; + sum_q2_sq += q2 * q2; + + double sampled_mK = rec.secondary_masses[0]; + int b = (int)((q2 - q2lo) / bw); + if (b < 0) b = 0; + if (b >= NB) b = NB - 1; + if (std::abs(sampled_mK - mK) < std::abs(sampled_mK - mKstar)) countK[b]++; + else countKstar[b]++; } - double mean_q2 = sum_q2 / Nsamp; - std::cout << " Mean Q² from SampleFinalState (" << Nsamp << " events): " << mean_q2 << std::endl; - std::cout << " Expected (correct): ~0.58" << std::endl; + double mean_sampled = sum_q2 / Nsamp; + double var_sampled = sum_q2_sq / Nsamp - mean_sampled * mean_sampled; + double stderr_mean = std::sqrt(var_sampled / Nsamp); - // If mean Q² > 0.7, the CDF sampling is biased - if (mean_q2 > 0.7) { - std::cout << " *** Q² BIAS CONFIRMED: " << mean_q2 << " >> 0.58 ***" << std::endl; + // --- (1) Normalization: integral of FinalStateProbability over q^2, + // summed over the K and K* mixture, must equal 1. --- + auto fsp_integral = [&](double comp_mK) -> double { + std::function integrand = [&](double q2) -> double { + InteractionRecord r = make_record_at_q2(sig, mD, comp_mK, ml, q2); + return decay.FinalStateProbability(r); + }; + double a = ml * ml; + double b = (mD - comp_mK) * (mD - comp_mK); + return rombergIntegrate(integrand, a, b); + }; + double normK = fsp_integral(mK); + double normKstar = fsp_integral(mKstar); + double total_norm = normK + normKstar; + EXPECT_NEAR(total_norm, 1.0, 1e-2); + + // --- (2) Closure of the mean: mean_density (in-test quadrature of + // FinalStateProbability) must match mean_sampled within MC error. --- + auto fsp_q2_integral = [&](double comp_mK) -> double { + std::function integrand = [&](double q2) -> double { + InteractionRecord r = make_record_at_q2(sig, mD, comp_mK, ml, q2); + return q2 * decay.FinalStateProbability(r); + }; + double a = ml * ml; + double b = (mD - comp_mK) * (mD - comp_mK); + return rombergIntegrate(integrand, a, b); + }; + double mean_density = (fsp_q2_integral(mK) + fsp_q2_integral(mKstar)) / total_norm; + EXPECT_NEAR(mean_sampled, mean_density, 4.0 * stderr_mean); + + // --- (3) Bin-by-bin closure per sub-population. For each filled bin the + // empirical density (count/(N*bw), with the sub-population folded + // in via N = total) must match FinalStateProbability at the bin + // center within ~3 sigma Poisson error. --- + for (int b = 0; b < NB; ++b) { + double q2c = q2lo + (b + 0.5) * bw; + // K sub-population + if (countK[b] > 30 && q2c < (mD - mK) * (mD - mK)) { + double emp = (double)countK[b] / (Nsamp * bw); + double sigma = std::sqrt((double)countK[b]) / (Nsamp * bw); + InteractionRecord r = make_record_at_q2(sig, mD, mK, ml, q2c); + double pred = decay.FinalStateProbability(r); + EXPECT_NEAR(emp, pred, 4.0 * sigma + 0.02 * pred); + } + // K* sub-population + if (countKstar[b] > 30 && q2c < (mD - mKstar) * (mD - mKstar)) { + double emp = (double)countKstar[b] / (Nsamp * bw); + double sigma = std::sqrt((double)countKstar[b]) / (Nsamp * bw); + InteractionRecord r = make_record_at_q2(sig, mD, mKstar, ml, q2c); + double pred = decay.FinalStateProbability(r); + EXPECT_NEAR(emp, pred, 4.0 * sigma + 0.02 * pred); + } } +} - // Loose check — correct value is 0.58, biased is ~0.79 - // This test documents the known bias rather than asserting correctness - EXPECT_GT(mean_q2, 0.3); // sanity: not negative/zero - EXPECT_LT(mean_q2, 1.2); // sanity: not absurdly high +// --- Test 4: TotalDecayWidthForFinalState fails loudly on bad signatures -- + +TEST(CharmMesonDecay, UnsupportedSignaturesThrow) { + CharmMesonDecay decay(ParticleType::D0); + + // Positive control: a valid D0 signature has positive width. + auto sigs = decay.GetPossibleSignaturesFromParent(ParticleType::D0); + InteractionRecord good; + good.signature = sigs[0]; + double w = 0.0; + EXPECT_NO_THROW(w = decay.TotalDecayWidthForFinalState(good)); + EXPECT_GT(w, 0.0); + + // Unsupported primary type. + InteractionRecord bad_primary; + bad_primary.signature.primary_type = ParticleType::PiPlus; + bad_primary.signature.target_type = ParticleType::Decay; + bad_primary.signature.secondary_types = {ParticleType::Hadrons}; + EXPECT_THROW(decay.TotalDecayWidthForFinalState(bad_primary), std::runtime_error); + + // Matched primary (D0) but a signature with no implemented mode. + InteractionRecord bad_secondaries; + bad_secondaries.signature.primary_type = ParticleType::D0; + bad_secondaries.signature.target_type = ParticleType::Decay; + bad_secondaries.signature.secondary_types = {ParticleType::PiPlus, ParticleType::PiMinus}; + EXPECT_THROW(decay.TotalDecayWidthForFinalState(bad_secondaries), std::runtime_error); } diff --git a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h index b1b3d6345..3eb21f386 100644 --- a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h +++ b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h @@ -35,7 +35,15 @@ class CharmMesonDecay : public Decay { friend cereal::access; private: const std::set primary_types = {siren::dataclasses::Particle::ParticleType::D0, siren::dataclasses::Particle::ParticleType::DPlus, siren::dataclasses::Particle::ParticleType::DsPlus, siren::dataclasses::Particle::ParticleType::D0Bar, siren::dataclasses::Particle::ParticleType::DMinus, siren::dataclasses::Particle::ParticleType::DsMinus}; - siren::utilities::Interpolator1D inverseCdf; // for dGamma + siren::utilities::Interpolator1D inverseCdf; // for dGamma (form-factor model; not used by FinalStateProbability) + // Shared closure helpers: SampleFinalState's density and FinalStateProbability + // both build on these, so Sample == Density by construction. + static double KStarMass(); + double VAWeightAngleAverage(double mD, double mK, double ml, double m23) const; + double SampledQ2Density(double mD, double mK, double ml, double q2, bool apply_va) const; + double SampledQ2Normalization(double mD, double mK, double ml, bool apply_va) const; + // Per-component normalization cache (not serialized; keyed by mass set). + mutable std::map norm_cache; public: CharmMesonDecay(); CharmMesonDecay(siren::dataclasses::Particle::ParticleType primary); diff --git a/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h b/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h index 5f0c4e1e8..54711c80c 100644 --- a/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h +++ b/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h @@ -2,7 +2,7 @@ #ifndef SIREN_CharmMesonDecay3Body_H #define SIREN_CharmMesonDecay3Body_H -// CharmMesonDecay3Body — Pythia-style 3-body phase-space decay for D mesons +// CharmMesonDecay3Body -- Pythia-style 3-body phase-space decay for D mesons // // Sister class to CharmMesonDecay (the legacy 2-body-cascade implementation). // Both inherit from Decay and share the same decay-width machinery @@ -46,7 +46,15 @@ class CharmMesonDecay3Body : public Decay { friend cereal::access; private: const std::set primary_types = {siren::dataclasses::Particle::ParticleType::D0, siren::dataclasses::Particle::ParticleType::DPlus}; - siren::utilities::Interpolator1D inverseCdf; // for dGamma (used in FinalStateProbability) + siren::utilities::Interpolator1D inverseCdf; // for dGamma (form-factor model; not used by FinalStateProbability) + // Shared closure helpers: SampleFinalState's density and FinalStateProbability + // both build on these, so Sample == Density by construction. + static double KStarMass(); + double VAWeightAngleAverage(double mD, double mK, double ml, double m23) const; + double SampledQ2Density(double mD, double mK, double ml, double q2, bool apply_va) const; + double SampledQ2Normalization(double mD, double mK, double ml, bool apply_va) const; + // Per-component normalization cache (not serialized; keyed by mass set). + mutable std::map norm_cache; public: CharmMesonDecay3Body(); CharmMesonDecay3Body(siren::dataclasses::Particle::ParticleType primary); From 5686bff8abb2b55b0a09b80edae4968ea5dc7742 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 10:59:07 -0500 Subject: [PATCH 69/93] DMesonELoss: bound inelasticity sampler to the density support and cap retries SampleFinalState rejects inelasticity z outside [0.001, 0.999] to match the truncated Gaussian in the density, so Sample == Density with no energy gain. The rejection loop is capped and throws InjectionFailure for a D meson at or below threshold. --- projects/interactions/private/DMesonELoss.cxx | 53 ++++++++++++++++--- .../public/SIREN/interactions/DMesonELoss.h | 7 +++ 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/projects/interactions/private/DMesonELoss.cxx b/projects/interactions/private/DMesonELoss.cxx index 5242676b5..f439d40a0 100644 --- a/projects/interactions/private/DMesonELoss.cxx +++ b/projects/interactions/private/DMesonELoss.cxx @@ -20,6 +20,7 @@ #include "SIREN/utilities/Random.h" // for SIREN_random #include "SIREN/utilities/Constants.h" // for electronMass #include "SIREN/utilities/Integration.h" // for rombergInt... +#include "SIREN/utilities/Errors.h" // for InjectionFailure namespace siren { @@ -119,6 +120,14 @@ double DMesonELoss::DifferentialCrossSection(dataclasses::InteractionRecord cons double final_energy = interaction.secondary_momenta[0][0]; double z = 1 - final_energy / primary_energy; + // The density is the truncated Gaussian in z normalized over [z_min_, z_max_]. + // Zero out-of-support records so the density support matches the sampler support + // (the sampler rejects z outside [z_min_, z_max_] as well). This keeps closure + // even for externally-constructed records with z < 0 (D meson gaining energy). + if(z < z_min_ || z > z_max_) { + return 0.0; + } + // now normalize the gaussian double total_xsec = TotalCrossSection(interaction.signature.primary_type, primary_energy); double z0 = 0.56; @@ -126,7 +135,7 @@ double DMesonELoss::DifferentialCrossSection(dataclasses::InteractionRecord cons std::function integrand = [&] (double z) -> double { return exp(-(pow(z - z0, 2))/(2 * pow(sigma, 2))); }; - double unnormalized = siren::utilities::rombergIntegrate(integrand, 0.001, 0.999); + double unnormalized = siren::utilities::rombergIntegrate(integrand, z_min_, z_max_); double normalization = total_xsec / unnormalized; double diff_xsec = normalization * exp(-(pow(z - z0, 2))/(2 * pow(sigma, 2))); @@ -148,6 +157,16 @@ void DMesonELoss::SampleFinalState(dataclasses::CrossSectionDistributionRecord& rk::P4 p1_lab; p1_lab = p1; primary_energy = p1_lab.e(); + + // Below the D meson mass there is no kinematically valid final state (the + // D meson would need energy >= Dmass), and the reject loop would spin for an + // astronomically large number of trials. Short-circuit with a recoverable + // InjectionFailure so the Injector can retry the attempt instead of hanging. + if (primary_energy <= Dmass) { + throw siren::utilities::InjectionFailure( + "DMesonELoss::SampleFinalState: primary energy below D meson mass; no valid final state"); + } + // sample an inelasticity from gaussian using Box-Muller Transform double sigma = 0.2; double z0 = 0.56; // for mesons only, for baryons it's 0.59, but not implemented yet @@ -155,8 +174,17 @@ void DMesonELoss::SampleFinalState(dataclasses::CrossSectionDistributionRecord& double final_energy; bool accept; + // Cap the rejection loop so a degenerate acceptance rate (primary energy just + // above Dmass) terminates deterministically instead of hanging. Mirrors the + // QuarkDISFromSpline reject-loop trial cap. + const int max_trials = 1000; + int trials = 0; do { + if (++trials > max_trials) { + throw siren::utilities::InjectionFailure( + "DMesonELoss::SampleFinalState: failed to sample inelasticity within trial cap"); + } do { u1 = random->Uniform(0, 1); @@ -166,12 +194,16 @@ void DMesonELoss::SampleFinalState(dataclasses::CrossSectionDistributionRecord& double z = sigma * sqrt(-2.0 * log(u1)) * cos(2.0 * M_PI * u2) + z0; // now modify the energy of the charm hadron and the corresponding momentum final_energy = primary_energy * (1-z); - if (pow(final_energy, 2) - pow(Dmass, 2) >= 0) { - accept = true; - } else { - accept = false; - } + // Reject z outside [z_min_, z_max_] so the realized sampling density is the + // truncated Gaussian that DifferentialCrossSection/FinalStateProbability + // normalize over the same interval (closure). z < z_min_ would otherwise let + // the D meson GAIN energy (final_energy > primary_energy). The kinematic cut + // final_energy^2 >= Dmass^2 is kept as a defensive guard. + accept = (z >= z_min_) && (z <= z_max_) && + (pow(final_energy, 2) - pow(Dmass, 2) >= 0); } while (!accept); + // Guaranteed by z >= z_min_ > 0: the D meson loses energy. + assert(final_energy <= primary_energy); double p3f = sqrt(pow(final_energy, 2) - pow(Dmass, 2)); double p3i = std::sqrt(std::pow(p1.px(), 2) + std::pow(p1.py(), 2) + std::pow(p1.pz(), 2)); double p_ratio = p3f / p3i; @@ -204,6 +236,15 @@ double DMesonELoss::FinalStateProbability(dataclasses::InteractionRecord const & } } +// NOTE: SampleFinalState samples a single inelasticity DOF z (the D meson is set +// collinear with the parent, so there is no independent azimuth). That same z is +// fully captured by the density: DifferentialCrossSection reconstructs +// z = 1 - final_energy/primary_energy and FinalStateProbability returns the +// (truncated, normalized) Gaussian in z. Sampled DOF == density DOF, so this class +// is closure-safe in the standard unbiased configuration (the same cross-section +// object supplies both the injection and physical densities). Like the other charm +// cross sections here, BIASING the D kinematics with a separate phase-space channel +// is NOT supported and would produce incorrect weights. std::vector DMesonELoss::DensityVariables() const { return std::vector{"Bjorken y"}; } diff --git a/projects/interactions/public/SIREN/interactions/DMesonELoss.h b/projects/interactions/public/SIREN/interactions/DMesonELoss.h index fd5d39f6e..2899fe89d 100644 --- a/projects/interactions/public/SIREN/interactions/DMesonELoss.h +++ b/projects/interactions/public/SIREN/interactions/DMesonELoss.h @@ -40,6 +40,13 @@ friend cereal::access; std::set primary_types_ = {siren::dataclasses::Particle::ParticleType::D0, siren::dataclasses::Particle::ParticleType::DPlus, siren::dataclasses::Particle::ParticleType::DsPlus, siren::dataclasses::Particle::ParticleType::D0Bar, siren::dataclasses::Particle::ParticleType::DMinus, siren::dataclasses::Particle::ParticleType::DsMinus}; std::set target_types_ = {siren::dataclasses::Particle::ParticleType::PPlus}; + // Truncation bounds for the inelasticity z. Single source of truth shared by + // SampleFinalState (rejection) and DifferentialCrossSection/FinalStateProbability + // (normalization), so the sampler support and the density support match exactly + // (closure). The Gaussian in z is normalized over [z_min_, z_max_]. + static constexpr double z_min_ = 0.001; + static constexpr double z_max_ = 0.999; + public: DMesonELoss(); From 6027ca668e1f836c25538fb391070b3515658006 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 10:59:07 -0500 Subject: [PATCH 70/93] PythiaDISCrossSection: renormalize fragmentation fractions and ASCII cleanup Renormalize the fragmentation fractions to sum to 1, matching QuarkDISFromSpline, and replace non-ASCII comment characters. --- .../private/PythiaDISCrossSection.cxx | 66 ++++++++++--------- .../interactions/PythiaDISCrossSection.h | 2 +- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/projects/interactions/private/PythiaDISCrossSection.cxx b/projects/interactions/private/PythiaDISCrossSection.cxx index 928301217..a815a2679 100644 --- a/projects/interactions/private/PythiaDISCrossSection.cxx +++ b/projects/interactions/private/PythiaDISCrossSection.cxx @@ -43,7 +43,7 @@ bool kinematicallyAllowed(double x, double y, double E, double M, double m) { } } // anonymous namespace -// ── SIRENRndm: bridges SIREN RNG into Pythia ── +// --- SIRENRndm: bridges SIREN RNG into Pythia --- class SIRENRndm : public Pythia8::RndmEngine { public: @@ -51,7 +51,7 @@ class SIRENRndm : public Pythia8::RndmEngine { double flat() override { return rng_->Uniform(0.0, 1.0); } }; -// ── Constructors ── +// --- Constructors --- PythiaDISCrossSection::PythiaDISCrossSection() {} @@ -105,7 +105,7 @@ PythiaDISCrossSection::PythiaDISCrossSection( SetUnits(units); } -// ── File I/O ── +// --- File I/O --- void PythiaDISCrossSection::SetUnits(std::string units) { std::transform(units.begin(), units.end(), units.begin(), @@ -144,7 +144,7 @@ void PythiaDISCrossSection::ReadParamsFromSplineTable() { if(!mass_good) target_mass_ = siren::utilities::Constants::isoscalarMass; } -// ── Equality ── +// --- Equality --- bool PythiaDISCrossSection::equal(CrossSection const & other) const { const PythiaDISCrossSection* x = dynamic_cast(&other); @@ -153,7 +153,7 @@ bool PythiaDISCrossSection::equal(CrossSection const & other) const { == std::tie(x->interaction_type_, x->target_mass_, x->minimum_Q2_, x->signatures_, x->primary_types_, x->target_types_); } -// ── Particle ID helpers ── +// --- Particle ID helpers --- bool PythiaDISCrossSection::IsCharmedHadron(int pdgId) { int abs_id = std::abs(pdgId); @@ -162,7 +162,7 @@ bool PythiaDISCrossSection::IsCharmedHadron(int pdgId) { } siren::dataclasses::ParticleType PythiaDISCrossSection::PdgToParticleType(int pdgId) { - // Direct cast — SIREN ParticleType enum values match PDG codes + // Direct cast -- SIREN ParticleType enum values match PDG codes return static_cast(pdgId); } @@ -191,7 +191,7 @@ double PythiaDISCrossSection::GetHadronMass(siren::dataclasses::ParticleType had } std::map PythiaDISCrossSection::getIndices(siren::dataclasses::InteractionSignature signature) { - // Identify meson by elimination (not via isD(), which only covers D0/D±). + // Identify meson by elimination (not via isD(), which only covers D0/D+/-). // First pass: claim lepton and Hadrons. Second pass: whatever remains is the meson. int lepton_id = -1, hadron_id = -1, meson_id = -1; for (size_t i = 0; i < signature.secondary_types.size(); i++) { @@ -209,7 +209,7 @@ std::map PythiaDISCrossSection::getIndices(siren::dataclasses: return {{"lepton", lepton_id}, {"hadron", hadron_id}, {"meson", meson_id}}; } -// ── Signatures ── +// --- Signatures --- void PythiaDISCrossSection::InitializeSignatures() { signatures_.clear(); @@ -250,11 +250,11 @@ void PythiaDISCrossSection::InitializeSignatures() { // Hadron remnant signature.secondary_types.push_back(siren::dataclasses::ParticleType::Hadrons); - // Charmed meson types. For ν the c quark fragments to D0/D+/Ds+; for ν̄ the - // c̄ quark fragments to D̄0/D-/Ds-. SampleFinalState writes Pythia's actual - // produced PID into the signature's meson slot, so the registered set must - // include the correct charge to keep weighter signature lookups in range - // (otherwise event_weight comes out NaN — see fix in this commit). + // Charmed meson types. For nu the c quark fragments to D0/D+/Ds+; for nubar + // the cbar quark fragments to Dbar0/D-/Ds-. SampleFinalState writes Pythia's + // actual produced PID into the signature's meson slot, so the registered set + // must include the correct charge to keep weighter signature lookups in range + // (otherwise event_weight comes out NaN -- see fix in this commit). // TODO: Add Lambda_c (4122) support. bool is_antineutrino = (primary_type == siren::dataclasses::ParticleType::NuEBar || @@ -283,7 +283,7 @@ void PythiaDISCrossSection::InitializeSignatures() { } } -// ── Cross sections (from splines, same as QuarkDISFromSpline) ── +// --- Cross sections (from splines, same as QuarkDISFromSpline) --- double PythiaDISCrossSection::TotalCrossSection(dataclasses::InteractionRecord const & interaction) const { siren::dataclasses::ParticleType primary_type = interaction.signature.primary_type; @@ -374,18 +374,23 @@ double PythiaDISCrossSection::InteractionThreshold(dataclasses::InteractionRecor return 0; } -// ── Fragmentation fractions ── +// --- Fragmentation fractions --- double PythiaDISCrossSection::FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const { - // Approximate fractions from Pythia (charm hadronization) + // Approximate fractions from Pythia (charm hadronization), renormalized to + // sum to 1.0 over the implemented D species. Raw fractions D0:D+/-:Ds = + // 0.60:0.23:0.15 sum to 0.98 because the Lambda_c channel is not modeled; the + // unmodeled Lambda_c fraction is redistributed by dividing each by 0.98 so the + // partitioned signatures exactly recover the inclusive charm cross section. + // Values kept in lockstep with QuarkDISFromSpline::FragmentationFraction. if (secondary == siren::dataclasses::ParticleType::D0 || secondary == siren::dataclasses::ParticleType::D0Bar) { - return 0.6; + return 0.6 / 0.98; } else if (secondary == siren::dataclasses::ParticleType::DPlus || secondary == siren::dataclasses::ParticleType::DMinus) { - return 0.23; + return 0.23 / 0.98; } else if (secondary == siren::dataclasses::ParticleType::DsPlus || secondary == siren::dataclasses::ParticleType::DsMinus) { - return 0.15; + return 0.15 / 0.98; } - // TODO: Add Lambda_c (~0.09) when signatures include them + // Lambda_c (~0.09) not yet implemented; its fraction is folded into the above. return 0; } @@ -404,7 +409,7 @@ double PythiaDISCrossSection::FinalStateProbability(dataclasses::InteractionReco return result; } -// ── Signature accessors ── +// --- Signature accessors --- std::vector PythiaDISCrossSection::GetPossiblePrimaries() const { return std::vector(primary_types_.begin(), primary_types_.end()); @@ -434,19 +439,16 @@ std::vector PythiaDISCrossSection::DensityVariables() const { return std::vector{"Bjorken x", "Bjorken y"}; } -// ══════════════════════════════════════════════════════════════════════ -// Pythia initialization and SampleFinalState — the core new logic -// ══════════════════════════════════════════════════════════════════════ +// ====================================================================== +// Pythia initialization and SampleFinalState -- the core new logic +// ====================================================================== void PythiaDISCrossSection::InitializePythia(double E_nu, int target_pdg) const { - // Ensure LHAPDF can find PDF sets — derive data path from the LHAPDF library location + // Ensure LHAPDF can find PDF sets -- derive data path from the LHAPDF library location // The pdf_set_ is e.g. "LHAPDF6:HERAPDF20_NLO_EIG", and LHAPDF needs LHAPDF_DATA_PATH set const char* lhapdf_path = std::getenv("LHAPDF_DATA_PATH"); if (!lhapdf_path) { - throw std::runtime_error("LHAPDF_DATA_PATH is not set"); - // Commented code below sets a default path on the Harvard FASRC cluster - // std::string default_path = "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/pzhelnin/LHAPDF/new_install/share/LHAPDF"; - // setenv("LHAPDF_DATA_PATH", default_path.c_str(), 0); + throw std::runtime_error("LHAPDF_DATA_PATH is not set; set it to your LHAPDF data directory"); } pythia_ = std::make_unique(pythia_data_path_, false); @@ -479,7 +481,7 @@ void PythiaDISCrossSection::InitializePythia(double E_nu, int target_pdg) const pythia_->settings.forceParm("StandardModel:Vus", 0.0); pythia_->settings.forceParm("StandardModel:Vub", 0.0); pythia_->settings.forceParm("StandardModel:Vcb", 0.0); - // Keeps Vcd ~ 0.225 and Vcs ~ 0.973 → every CC event produces charm + // Keeps Vcd ~ 0.225 and Vcs ~ 0.973 -> every CC event produces charm } // PDF @@ -506,7 +508,7 @@ void PythiaDISCrossSection::InitializePythia(double E_nu, int target_pdg) const } // Bridge SIREN RNG into Pythia for reproducibility. - // Must be done after init() — init uses Pythia's internal RNG for setup. + // Must be done after init() -- init uses Pythia's internal RNG for setup. // The SIREN RNG is connected here and updated per-event in SampleFinalState. siren_rndm_ = std::make_shared(); pythia_->rndm.rndmEnginePtr(siren_rndm_); @@ -603,7 +605,7 @@ void PythiaDISCrossSection::SampleFinalState(dataclasses::CrossSectionDistributi geom3::UnitVector3 z_dir = geom3::UnitVector3::zAxis(); geom3::Rotation3 rot = geom3::rotationBetween(z_dir, nu_dir); - // Helper lambda to convert Pythia Vec4 → rotated rk::P4 + // Helper lambda to convert Pythia Vec4 -> rotated rk::P4 auto pythia_to_siren = [&](const Pythia8::Vec4 & pv, double mass) -> rk::P4 { geom3::Vector3 mom(pv.px(), pv.py(), pv.pz()); mom = rot * mom; diff --git a/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h b/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h index 39bc166da..6ee70d22f 100644 --- a/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h +++ b/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h @@ -38,7 +38,7 @@ namespace siren { namespace utilities { class SIREN_random; } } namespace siren { namespace interactions { -// Forward declaration — defined in .cxx +// Forward declaration -- defined in .cxx class SIRENRndm; class PythiaDISCrossSection : public CrossSection { From 6f754ae02cc3034a586e80c3169fe575da27999a Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 10:59:07 -0500 Subject: [PATCH 71/93] Add charm-DIS unit tests, detector degenerate-direction regression, and gated python tests New gtests cover DMesonELoss, CharmMesonDecay3Body, and the QuarkDIS density-variable contract, plus Vector3D and DetectorModel degenerate-direction regressions. The slow-rescaling smoke scripts become env-gated pytest tests that skip without SIREN_CHARM_SPLINE_DIR. --- projects/detector/CMakeLists.txt | 1 + .../DetectorModelDegenerateDirection_TEST.cxx | 212 ++++++++++ projects/interactions/CMakeLists.txt | 10 + .../test/CharmMesonDecay3Body_TEST.cxx | 349 +++++++++++++++++ .../private/test/DMesonELoss_TEST.cxx | 271 +++++++++++++ .../test/QuarkDISDensityContract_TEST.cxx | 78 ++++ projects/math/private/test/Vector3D_TEST.cxx | 59 +++ tests/python/test_quarkdis_slow_rescaling.py | 364 ++++++++++++++++++ tests/slow_rescaling/smoke_quarkdis_100.py | 188 --------- tests/slow_rescaling/smoke_quarkdis_10k.py | 295 -------------- 10 files changed, 1344 insertions(+), 483 deletions(-) create mode 100644 projects/detector/private/test/DetectorModelDegenerateDirection_TEST.cxx create mode 100644 projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx create mode 100644 projects/interactions/private/test/DMesonELoss_TEST.cxx create mode 100644 projects/interactions/private/test/QuarkDISDensityContract_TEST.cxx create mode 100644 tests/python/test_quarkdis_slow_rescaling.py delete mode 100644 tests/slow_rescaling/smoke_quarkdis_100.py delete mode 100644 tests/slow_rescaling/smoke_quarkdis_10k.py diff --git a/projects/detector/CMakeLists.txt b/projects/detector/CMakeLists.txt index d6086f205..c471ddd0f 100644 --- a/projects/detector/CMakeLists.txt +++ b/projects/detector/CMakeLists.txt @@ -65,6 +65,7 @@ package_add_test(UnitTest_Axis ${PROJECT_SOURCE_DIR}/projects/detector/private/t package_add_test(UnitTest_DensityDistribution ${PROJECT_SOURCE_DIR}/projects/detector/private/test/DensityDistribution_TEST.cxx) package_add_test(UnitTest_Distribution1D ${PROJECT_SOURCE_DIR}/projects/detector/private/test/Distribution1D_TEST.cxx) package_add_test(UnitTest_DetectorModel ${PROJECT_SOURCE_DIR}/projects/detector/private/test/DetectorModel_TEST.cxx) +package_add_test(UnitTest_DetectorModelDegenerateDirection ${PROJECT_SOURCE_DIR}/projects/detector/private/test/DetectorModelDegenerateDirection_TEST.cxx) package_add_test(UnitTest_MaterialModel ${PROJECT_SOURCE_DIR}/projects/detector/private/test/MaterialModel_TEST.cxx) package_add_test(UnitTest_Path ${PROJECT_SOURCE_DIR}/projects/detector/private/test/Path_TEST.cxx) package_add_test(UnitTest_GDMLParser ${PROJECT_SOURCE_DIR}/projects/detector/private/test/GDMLParser_TEST.cxx) diff --git a/projects/detector/private/test/DetectorModelDegenerateDirection_TEST.cxx b/projects/detector/private/test/DetectorModelDegenerateDirection_TEST.cxx new file mode 100644 index 000000000..a005bc072 --- /dev/null +++ b/projects/detector/private/test/DetectorModelDegenerateDirection_TEST.cxx @@ -0,0 +1,212 @@ + +// Regression test for the degenerate trajectory-direction fix (commit 29e189f6). +// +// Root cause: DetectorModel computes a trajectory direction as (p1 - p0) in +// cartesian coordinates. When p0 and p1 are at Earth-scale coordinates and only +// micrometers apart, the subtraction suffers catastrophic cancellation, so the +// resulting direction is unreliable. The old code either (a) normalized a +// near-zero vector (dividing by ~0 -> NaN, since Vector3D::normalize() had no +// zero-length guard) or (b) reached assert(std::abs(1.0 - std::abs(dot)) < 1e-6) +// with a non-unit direction and aborted. +// +// The fix guards Vector3D::normalize() against zero length and makes the +// interaction-depth sub-threshold guard return the well-defined decay term +// (distance / total_decay_length) instead of normalizing an unreliable +// direction. +// +// These tests construct exactly the degenerate cases and assert the previously +// failing paths now return finite, sensible values and do not abort. They use a +// default (infinite-vacuum) DetectorModel, so they are fully self-contained and +// require no external data files. +// +// NOTE: the dot-product assert only fires in an assertions-enabled build +// (Debug / RelWithDebInfo). In a fully optimized NDEBUG build the regression +// would not abort even without the fix, but the finite/non-NaN return-value +// assertions below still exercise and lock in the corrected numeric behavior. + +#include +#include +#include + +#include + +#include "SIREN/geometry/Geometry.h" +#include "SIREN/detector/DetectorModel.h" +#include "SIREN/detector/Coordinates.h" +#include "SIREN/math/Vector3D.h" +#include "SIREN/dataclasses/Particle.h" + +using namespace siren::math; +using namespace siren::geometry; +using namespace siren::detector; +using namespace siren::dataclasses; + +namespace { + +// distance_threshold in DetectorModel is 1e-5 m; pick an offset well below it +// but large enough to be exactly representable next to an Earth-scale x value. +constexpr double kEarthScaleX = 6.371e6; // m, ~Earth radius +constexpr double kSubThresholdOffset = 1e-7; // m, << distance_threshold (1e-5) + +bool IsFinite(double x) { + return std::isfinite(x) && !std::isnan(x); +} + +} // namespace + +// p0 and p1 are distinct (so the p0 == p1 early-return does NOT fire) but their +// separation is below distance_threshold. GetInteractionDepthInCGS with non-empty +// targets must short-circuit to distance/total_decay_length rather than +// normalizing the cancellation-garbage direction and tripping the dot assert. +TEST(DetectorModelDegenerateDirection, InteractionDepthSubThresholdWithTargets) +{ + DetectorModel A; // infinite vacuum sphere, no files + + Vector3D p0; + p0.SetCartesianCoordinates(kEarthScaleX, 0.0, 0.0); + Vector3D p1; + p1.SetCartesianCoordinates(kEarthScaleX + kSubThresholdOffset, 0.0, 0.0); + + // Sanity: the early p0 == p1 guard must NOT catch this (points differ), + // and the separation is genuinely below threshold. + ASSERT_TRUE(p0 != p1); + double distance = (p1 - p0).magnitude(); + ASSERT_GT(distance, 0.0); + ASSERT_LE(distance, 1e-5); + + std::vector targets = {ParticleType::Nucleon}; + std::vector total_cross_sections = {1.0e-27}; // cm^2, arbitrary + double total_decay_length = 1.0e3; // m, arbitrary finite, nonzero + + double depth = std::numeric_limits::quiet_NaN(); + // Must NOT abort on assert(std::abs(1.0 - std::abs(dot)) < 1e-6). + ASSERT_NO_THROW(depth = A.GetInteractionDepthInCGS( + DetectorPosition(p0), DetectorPosition(p1), + targets, total_cross_sections, total_decay_length)); + + EXPECT_TRUE(IsFinite(depth)) << "InteractionDepth must be finite, got " << depth; + + // The fix returns the decay-only limit at sub-threshold distance, matching + // the targets.empty() branch (continuity across the threshold). + double expected = distance / total_decay_length; + EXPECT_NEAR(expected, depth, std::abs(expected) * 1e-9 + 1e-30); + EXPECT_GE(depth, 0.0); +} + +// With empty targets, GetInteractionDepthInCGS takes the decay-only branch. +// Confirm the sub-threshold result is continuous with that branch and finite. +TEST(DetectorModelDegenerateDirection, InteractionDepthSubThresholdDecayOnly) +{ + DetectorModel A; + + Vector3D p0; + p0.SetCartesianCoordinates(kEarthScaleX, 0.0, 0.0); + Vector3D p1; + p1.SetCartesianCoordinates(kEarthScaleX + kSubThresholdOffset, 0.0, 0.0); + + double distance = (p1 - p0).magnitude(); + std::vector targets; // empty -> decay only + std::vector total_cross_sections; + double total_decay_length = 2.5e2; + + double depth = std::numeric_limits::quiet_NaN(); + ASSERT_NO_THROW(depth = A.GetInteractionDepthInCGS( + DetectorPosition(p0), DetectorPosition(p1), + targets, total_cross_sections, total_decay_length)); + + EXPECT_TRUE(IsFinite(depth)); + EXPECT_NEAR(distance / total_decay_length, depth, + std::abs(distance / total_decay_length) * 1e-9 + 1e-30); +} + +// GetColumnDepthInCGS on a sub-threshold but distinct segment normalizes a tiny +// (magnitude ~1e-7) direction. Before the normalize zero-guard a truly zero +// difference produced NaN; here the difference is small-but-nonzero, so the call +// must complete without aborting and return a finite value. +TEST(DetectorModelDegenerateDirection, ColumnDepthSubThresholdIsFinite) +{ + DetectorModel A; + + Vector3D p0; + p0.SetCartesianCoordinates(kEarthScaleX, 0.0, 0.0); + Vector3D p1; + p1.SetCartesianCoordinates(kEarthScaleX + kSubThresholdOffset, 0.0, 0.0); + + double cd = std::numeric_limits::quiet_NaN(); + ASSERT_NO_THROW(cd = A.GetColumnDepthInCGS(DetectorPosition(p0), DetectorPosition(p1))); + EXPECT_TRUE(IsFinite(cd)) << "ColumnDepth must be finite, got " << cd; + EXPECT_GE(cd, 0.0); +} + +// GetParticleColumnDepth on the same sub-threshold segment must also return +// finite per-target values without aborting. +TEST(DetectorModelDegenerateDirection, ParticleColumnDepthSubThresholdIsFinite) +{ + DetectorModel A; + + Vector3D p0; + p0.SetCartesianCoordinates(kEarthScaleX, 0.0, 0.0); + Vector3D p1; + p1.SetCartesianCoordinates(kEarthScaleX + kSubThresholdOffset, 0.0, 0.0); + + std::vector targets = {ParticleType::Nucleon, ParticleType::EMinus}; + + std::vector counts; + Geometry::IntersectionList intersections = A.GetIntersections( + DetectorPosition(p0), DetectorDirection(Vector3D(1.0, 0.0, 0.0))); + ASSERT_NO_THROW(counts = A.GetParticleColumnDepth( + intersections, DetectorPosition(p0), DetectorPosition(p1), targets)); + + ASSERT_EQ(targets.size(), counts.size()); + for(double c : counts) { + EXPECT_TRUE(IsFinite(c)) << "ParticleColumnDepth entry must be finite, got " << c; + EXPECT_GE(c, 0.0); + } +} + +// Exact-coincidence case (p0 == p1) at Earth-scale coordinates. Here (p1 - p0) +// is the zero vector. The early p0 == p1 guard returns the degenerate limit, but +// the Vector3D::normalize() zero-guard is the safety net should any path reach +// it. All depth queries must return 0 / finite and not produce NaN. +TEST(DetectorModelDegenerateDirection, CoincidentPointsReturnZeroFinite) +{ + DetectorModel A; + + Vector3D v; + v.SetCartesianCoordinates(kEarthScaleX, 1.2e6, -3.4e5); + + // Column depth between identical points is exactly zero. + double cd = std::numeric_limits::quiet_NaN(); + ASSERT_NO_THROW(cd = A.GetColumnDepthInCGS(DetectorPosition(v), DetectorPosition(v))); + EXPECT_TRUE(IsFinite(cd)); + EXPECT_DOUBLE_EQ(0.0, cd); + + // Interaction depth between identical points is exactly zero (early guard). + std::vector targets = {ParticleType::Nucleon}; + std::vector total_cross_sections = {1.0e-27}; + double total_decay_length = 1.0e3; + double depth = std::numeric_limits::quiet_NaN(); + ASSERT_NO_THROW(depth = A.GetInteractionDepthInCGS( + DetectorPosition(v), DetectorPosition(v), + targets, total_cross_sections, total_decay_length)); + EXPECT_TRUE(IsFinite(depth)); + EXPECT_DOUBLE_EQ(0.0, depth); + + // Particle column depth between identical points is all zeros. + Geometry::IntersectionList intersections = A.GetIntersections( + DetectorPosition(v), DetectorDirection(Vector3D(1.0, 0.0, 0.0))); + std::vector counts; + ASSERT_NO_THROW(counts = A.GetParticleColumnDepth( + intersections, DetectorPosition(v), DetectorPosition(v), targets)); + ASSERT_EQ(targets.size(), counts.size()); + for(double c : counts) { + EXPECT_TRUE(IsFinite(c)); + EXPECT_DOUBLE_EQ(0.0, c); + } +} + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/projects/interactions/CMakeLists.txt b/projects/interactions/CMakeLists.txt index ea1ae29e3..8e6d95724 100644 --- a/projects/interactions/CMakeLists.txt +++ b/projects/interactions/CMakeLists.txt @@ -103,6 +103,16 @@ package_add_test(UnitTest_CharmMesonDecay ${PROJECT_SOURCE_DIR}/projects/interac # that exercises the real base-class CrossSection::TotalCrossSectionAllFinalStates). package_add_test(UnitTest_CharmDISClosure ${PROJECT_SOURCE_DIR}/projects/interactions/private/test/CharmDISClosure_TEST.cxx) +# Charm-meson energy-loss and 3-body charm-meson decay (no Pythia8/spline needed; +# both classes link only against SIREN core). +package_add_test(UnitTest_DMesonELoss ${PROJECT_SOURCE_DIR}/projects/interactions/private/test/DMesonELoss_TEST.cxx) +package_add_test(UnitTest_CharmMesonDecay3Body ${PROJECT_SOURCE_DIR}/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx) + +# QuarkDISFromSpline density-variable contract tripwire + fragmentation pdf +# normalization (no spline file needed: the no-arg ctor builds only the +# fragmentation pdf/cdf). +package_add_test(UnitTest_QuarkDISDensityContract ${PROJECT_SOURCE_DIR}/projects/interactions/private/test/QuarkDISDensityContract_TEST.cxx) + # Real PythiaDISCrossSection charm-DIS closure/normalization regression test. # Only built with Pythia8 support; needs charm-DIS spline files via env vars # (SIREN_PYTHIA_TEST_DSDXDY, SIREN_PYTHIA_TEST_SIGMA), otherwise it SKIPs. diff --git a/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx b/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx new file mode 100644 index 000000000..d9991eac0 --- /dev/null +++ b/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx @@ -0,0 +1,349 @@ +/** + * Unit test for CharmMesonDecay3Body -- Pythia-style 3-body phase-space decay + * of charm mesons with V-A matrix-element reweighting and K / K*(892) kinematic + * mixing. + * + * D (m0) -> K (m1) + lepton (m2) + neutrino (m3) + * + * All three daughters are constructed in the D rest frame and boosted to the + * lab with the SAME boost, so 4-momentum is conserved exactly (up to float). + * A per-event coin flip draws the hadron mass as either the pseudoscalar K mass + * or the K*(892) mass (= Constants::KPrimePlusMass via KStarMass()). + * + * Tests: + * 1. ThreeBodyEnergyMomentumConservation -- sum of the three secondary + * 4-momenta equals the primary 4-momentum component-wise. + * 2. DaughterMassShells -- lepton/neutrino on shell, hadron mass equals either + * the pseudoscalar K or the K*(892) mass, and m23 = sqrt((p_l+p_nu)^2) lies + * in the allowed window [ml, mD - mK]. + * 3. KStarMixingFraction -- the empirical K vs K* split matches the PDG-derived + * fracK within a few binomial sigma (D0 and D+). + * 4. TotalDecayWidthAndBranchingSums -- per-signature widths are positive, the + * three branching ratios partition unity, TotalDecayWidth equals the sum + * over signatures, and bad signatures throw. + * 5. FinalStateProbabilityClosure -- FinalStateProbability is non-negative, + * equals 1 for the fully hadronic mode, and the sampled q^2 distribution + * closes against FinalStateProbability bin-by-bin within MC error. + */ +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "SIREN/interactions/CharmMesonDecay3Body.h" +#include "SIREN/dataclasses/Particle.h" +#include "SIREN/dataclasses/InteractionRecord.h" +#include "SIREN/dataclasses/InteractionSignature.h" +#include "SIREN/utilities/Random.h" +#include "SIREN/utilities/Constants.h" + +using namespace siren::utilities; +using namespace siren::interactions; +using namespace siren::dataclasses; + +namespace { + +// Build an unfinalized 3-body semileptonic record for a boosted D meson. +InteractionRecord make_semilep_record(const InteractionSignature & sig, + double mD, double mK, double ml, double E_D) { + double p_D = std::sqrt(E_D * E_D - mD * mD); + InteractionRecord rec; + rec.signature = sig; + rec.primary_mass = mD; + rec.primary_momentum = {E_D, 0, 0, p_D}; + rec.primary_helicity = 0; + rec.target_mass = 0; + rec.target_helicity = 0; + rec.secondary_momenta = {{0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; + rec.secondary_masses = {mK, ml, 0.0}; + rec.secondary_helicities = {0, 0, 0}; + return rec; +} + +// q^2 = (p_D - p_K)^2 from a finalized record. +double reconstruct_q2(const InteractionRecord & rec) { + double qE = rec.primary_momentum[0] - rec.secondary_momenta[0][0]; + double qpx = rec.primary_momentum[1] - rec.secondary_momenta[0][1]; + double qpy = rec.primary_momentum[2] - rec.secondary_momenta[0][2]; + double qpz = rec.primary_momentum[3] - rec.secondary_momenta[0][3]; + return qE * qE - qpx * qpx - qpy * qpy - qpz * qpz; +} + +} // namespace + +// --- Test 1: exact 4-momentum conservation ---------------------------------- + +TEST(CharmMesonDecay3Body, ThreeBodyEnergyMomentumConservation) { + CharmMesonDecay3Body decay(ParticleType::D0); + auto sigs = decay.GetPossibleSignaturesFromParent(ParticleType::D0); + ASSERT_GE(sigs.size(), 2u); + + double mD = Constants::D0Mass; + double E_D = 100.0; + auto rng = std::make_shared(); + + // both semileptonic modes (e and mu) + std::vector> cases = { + {sigs[0], Constants::electronMass}, + {sigs[1], Constants::muonMass}, + }; + + for (auto & c : cases) { + const InteractionSignature & sig = c.first; + double ml = c.second; + for (int i = 0; i < 2000; ++i) { + InteractionRecord rec = make_semilep_record(sig, mD, Constants::KMinusMass, ml, E_D); + CrossSectionDistributionRecord cdr(rec); + decay.SampleFinalState(cdr, rng); + cdr.Finalize(rec); + ASSERT_EQ(rec.secondary_momenta.size(), 3u); + for (int comp = 0; comp < 4; ++comp) { + double s = rec.secondary_momenta[0][comp] + + rec.secondary_momenta[1][comp] + + rec.secondary_momenta[2][comp]; + EXPECT_NEAR(s, rec.primary_momentum[comp], 1e-5); + } + } + } +} + +// --- Test 2: daughter mass shells & m23 window ------------------------------ + +TEST(CharmMesonDecay3Body, DaughterMassShells) { + CharmMesonDecay3Body decay(ParticleType::D0); + auto sig = decay.GetPossibleSignaturesFromParent(ParticleType::D0)[0]; // D0 -> K- e+ nu + double mD = Constants::D0Mass; + double ml = Constants::electronMass; + double mK_base = Constants::KMinusMass; + double mKstar = Constants::KPrimePlusMass; // KStarMass() in the source + double E_D = 100.0; + auto rng = std::make_shared(); + + for (int i = 0; i < 5000; ++i) { + InteractionRecord rec = make_semilep_record(sig, mD, mK_base, ml, E_D); + CrossSectionDistributionRecord cdr(rec); + decay.SampleFinalState(cdr, rng); + cdr.Finalize(rec); + + const auto & pK = rec.secondary_momenta[0]; + const auto & pl = rec.secondary_momenta[1]; + const auto & pnu = rec.secondary_momenta[2]; + + auto mass = [](const std::array & p) { + double m2 = p[0]*p[0] - p[1]*p[1] - p[2]*p[2] - p[3]*p[3]; + return std::sqrt(std::max(0.0, m2)); + }; + + // lepton on shell, neutrino massless. + EXPECT_NEAR(mass(pl), ml, 1e-5); + EXPECT_NEAR(mass(pnu), 0.0, 1e-5); + + // hadron mass is one of the two mixture components. + double mK = mass(pK); + bool is_K = std::abs(mK - mK_base) < 1e-4; + bool is_Kstar = std::abs(mK - mKstar) < 1e-4; + EXPECT_TRUE(is_K || is_Kstar) << "hadron mass " << mK << " is neither K nor K*"; + + // m23 = (lepton + neutrino) invariant mass within [ml, mD - mK]. + std::array p23 = {pl[0]+pnu[0], pl[1]+pnu[1], pl[2]+pnu[2], pl[3]+pnu[3]}; + double m23 = mass(p23); + double m23max = mD - mK; + EXPECT_GE(m23, ml - 1e-4); + EXPECT_LE(m23, m23max + 1e-4); + } +} + +// --- Test 3: K / K*(892) mixing fraction ------------------------------------ + +TEST(CharmMesonDecay3Body, KStarMixingFraction) { + double mKstar = Constants::KPrimePlusMass; + auto rng = std::make_shared(); + int N = 20000; + + struct Case { + ParticleType primary; + double mD; + double mK_base; + double fracK; + }; + std::vector cases = { + {ParticleType::D0, Constants::D0Mass, Constants::KMinusMass, 3.41 / (3.41 + 2.17)}, + {ParticleType::DPlus, Constants::DPlusMass, Constants::K0Mass, 8.74 / (8.74 + 5.33)}, + }; + + for (auto & c : cases) { + CharmMesonDecay3Body decay(c.primary); + auto sig = decay.GetPossibleSignaturesFromParent(c.primary)[0]; + double ml = Constants::electronMass; + double E_D = 50.0; + long countK = 0; + for (int i = 0; i < N; ++i) { + InteractionRecord rec = make_semilep_record(sig, c.mD, c.mK_base, ml, E_D); + CrossSectionDistributionRecord cdr(rec); + decay.SampleFinalState(cdr, rng); + cdr.Finalize(rec); + double m2 = rec.secondary_momenta[0][0]*rec.secondary_momenta[0][0] + - rec.secondary_momenta[0][1]*rec.secondary_momenta[0][1] + - rec.secondary_momenta[0][2]*rec.secondary_momenta[0][2] + - rec.secondary_momenta[0][3]*rec.secondary_momenta[0][3]; + double mK = std::sqrt(std::max(0.0, m2)); + if (std::abs(mK - c.mK_base) < std::abs(mK - mKstar)) countK++; + } + double emp = (double)countK / N; + double sigma = std::sqrt(c.fracK * (1.0 - c.fracK) / N); + EXPECT_NEAR(emp, c.fracK, 5.0 * sigma); + } +} + +// --- Test 4: decay widths & branching bookkeeping --------------------------- + +TEST(CharmMesonDecay3Body, TotalDecayWidthAndBranchingSums) { + // D0: BR_semilep(e) = BR_semilep(mu) = 0.0558; hadronic remainder = 1 - 2*0.0558. + // D+: BR_semilep(e) = BR_semilep(mu) = 0.1407; hadronic remainder = 1 - 2*0.1407. + struct Case { ParticleType primary; double br_semilep; }; + std::vector cases = { + {ParticleType::D0, 0.0558}, + {ParticleType::DPlus, 0.1407}, + }; + + for (auto & c : cases) { + CharmMesonDecay3Body decay(c.primary); + auto sigs = decay.GetPossibleSignaturesFromParent(c.primary); + ASSERT_EQ(sigs.size(), 3u); // e-mode, mu-mode, hadronic + + double sum_width = 0.0; + for (auto & sig : sigs) { + InteractionRecord r; + r.signature = sig; + double w = 0.0; + EXPECT_NO_THROW(w = decay.TotalDecayWidthForFinalState(r)); + EXPECT_GT(w, 0.0); + sum_width += w; + } + + // The three branching ratios partition unity. + double br_sum = c.br_semilep + c.br_semilep + (1.0 - 2.0 * c.br_semilep); + EXPECT_NEAR(br_sum, 1.0, 1e-12); + + // TotalDecayWidth(primary) is the sum over GetPossibleSignaturesFromParent. + double total = decay.TotalDecayWidth(c.primary); + EXPECT_NEAR(total, sum_width, std::abs(total) * 1e-12 + 1e-30); + EXPECT_GT(total, 0.0); + } + + // Unsupported primary throws. + CharmMesonDecay3Body decay(ParticleType::D0); + InteractionRecord bad_primary; + bad_primary.signature.primary_type = ParticleType::PiPlus; + bad_primary.signature.target_type = ParticleType::Decay; + bad_primary.signature.secondary_types = {ParticleType::Hadrons}; + EXPECT_THROW(decay.TotalDecayWidthForFinalState(bad_primary), std::runtime_error); + + // Matched primary (D0) but an unimplemented set of secondaries throws (R4). + InteractionRecord bad_secondaries; + bad_secondaries.signature.primary_type = ParticleType::D0; + bad_secondaries.signature.target_type = ParticleType::Decay; + bad_secondaries.signature.secondary_types = {ParticleType::PiPlus, ParticleType::PiMinus, ParticleType::Pi0}; + EXPECT_THROW(decay.TotalDecayWidthForFinalState(bad_secondaries), std::runtime_error); +} + +// --- Test 5: FinalStateProbability sanity + q^2 closure --------------------- + +TEST(CharmMesonDecay3Body, FinalStateProbabilityClosure) { + CharmMesonDecay3Body decay(ParticleType::D0); + auto sigs = decay.GetPossibleSignaturesFromParent(ParticleType::D0); + auto sig = sigs[0]; // D0 -> K- e+ nu + + double mD = Constants::D0Mass; + double ml = Constants::electronMass; + double mK = Constants::KMinusMass; + double mKstar = Constants::KPrimePlusMass; + double E_D = 100.0; + auto rng = std::make_shared(); + + // (a) The fully hadronic catch-all has FinalStateProbability == 1. + { + auto had_sig = sigs[2]; // D0 -> Hadrons + InteractionRecord rec; + rec.signature = had_sig; + rec.primary_mass = mD; + rec.primary_momentum = {E_D, 0, 0, std::sqrt(E_D * E_D - mD * mD)}; + rec.secondary_momenta = {{0, 0, 0, 0}}; + rec.secondary_masses = {0.0}; + EXPECT_NEAR(decay.FinalStateProbability(rec), 1.0, 1e-12); + } + + // (b) Sample many semileptonic events, histogram q^2 separately for the K + // and K* sub-populations, and compare the empirical density per bin to + // fractions[comp] * FinalStateProbability evaluated at a record built at + // that q^2 with the matching hadron mass. + const int NB = 16; + double q2lo = 0.0; + double q2hi = (mD - mK) * (mD - mK); // widest support (K mass) + double bw = (q2hi - q2lo) / NB; + std::vector countK(NB, 0), countKstar(NB, 0); + const int N = 40000; + + for (int i = 0; i < N; ++i) { + InteractionRecord rec = make_semilep_record(sig, mD, mK, ml, E_D); + CrossSectionDistributionRecord cdr(rec); + decay.SampleFinalState(cdr, rng); + cdr.Finalize(rec); + + double q2 = reconstruct_q2(rec); + double sampled_mK = rec.secondary_masses[0]; + double fsp = decay.FinalStateProbability(rec); + EXPECT_GE(fsp, 0.0); + + int b = (int)((q2 - q2lo) / bw); + if (b < 0) b = 0; + if (b >= NB) b = NB - 1; + if (std::abs(sampled_mK - mK) < std::abs(sampled_mK - mKstar)) countK[b]++; + else countKstar[b]++; + } + + // Build a record at a target q^2 in the D rest frame with hadron mass comp_mK + // and evaluate FinalStateProbability there (it already includes the K/K* + // mixture weight fractions[comp]). + auto fsp_at_q2 = [&](double comp_mK, double q2) -> double { + double EK = (mD * mD + comp_mK * comp_mK - q2) / (2 * mD); + double pk_sq = EK * EK - comp_mK * comp_mK; + if (pk_sq < 0) return 0.0; + double PK = std::sqrt(pk_sq); + InteractionRecord r; + r.signature = sig; + r.primary_mass = mD; + r.primary_momentum = {mD, 0, 0, 0}; // D at rest (q^2 frame-independent) + r.secondary_momenta = {{EK, PK, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; + r.secondary_masses = {comp_mK, ml, 0.0}; + return decay.FinalStateProbability(r); + }; + + for (int b = 0; b < NB; ++b) { + double q2c = q2lo + (b + 0.5) * bw; + if (countK[b] > 40 && q2c < (mD - mK) * (mD - mK)) { + double emp = (double)countK[b] / (N * bw); + double sigma = std::sqrt((double)countK[b]) / (N * bw); + double pred = fsp_at_q2(mK, q2c); + EXPECT_NEAR(emp, pred, 4.0 * sigma + 0.03 * pred); + } + if (countKstar[b] > 40 && q2c < (mD - mKstar) * (mD - mKstar)) { + double emp = (double)countKstar[b] / (N * bw); + double sigma = std::sqrt((double)countKstar[b]) / (N * bw); + double pred = fsp_at_q2(mKstar, q2c); + EXPECT_NEAR(emp, pred, 4.0 * sigma + 0.03 * pred); + } + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/projects/interactions/private/test/DMesonELoss_TEST.cxx b/projects/interactions/private/test/DMesonELoss_TEST.cxx new file mode 100644 index 000000000..d73c68e55 --- /dev/null +++ b/projects/interactions/private/test/DMesonELoss_TEST.cxx @@ -0,0 +1,271 @@ +/** + * Unit test for DMesonELoss (charm-meson energy-loss "cross section"). + * + * DMesonELoss models a D meson losing a fraction z of its energy to a hadronic + * vertex. The signature is D -> {same D, Hadrons} on a PPlus target. The + * inelasticity z is drawn from a Gaussian (z0=0.56, sigma=0.2) truncated to + * [z_min_, z_max_] = [0.001, 0.999] via rejection; the same truncated Gaussian + * is the density returned by DifferentialCrossSection / FinalStateProbability, + * so Sample == Density (closure). + * + * Tests: + * 1. EnergyMonotonicallyDecreases -- outgoing D energy is always in + * (D mass, primary energy); the D never gains energy. + * 2. FourMomentumAndMassInvariants -- outgoing D is on its mass shell, + * energy is conserved (E0 == E_D + E_H), and the two outgoing 3-momenta + * are collinear with the incoming direction (momentum is NOT conserved by + * design -- the hadron is massless and collinear). + * 3. TotalCrossSectionPositiveAndThrows -- TotalCrossSection is positive and + * increasing in E over the validity range, and throws on an unsupported + * primary. + * 4. SubThresholdThrows -- SampleFinalState throws siren::utilities:: + * InjectionFailure when the primary energy is at or below the D mass. + * 5. ZDensityClosure -- the empirical sampled-z distribution matches + * FinalStateProbability/DifferentialCrossSection (the truncated Gaussian) + * bin-by-bin within Poisson error (closure of the realized sampler against + * the advertised density). + */ +#include +#include +#include +#include + +#include + +#include "SIREN/interactions/DMesonELoss.h" +#include "SIREN/dataclasses/Particle.h" +#include "SIREN/dataclasses/InteractionRecord.h" +#include "SIREN/dataclasses/InteractionSignature.h" +#include "SIREN/utilities/Random.h" +#include "SIREN/utilities/Constants.h" +#include "SIREN/utilities/Errors.h" + +using namespace siren::utilities; +using namespace siren::interactions; +using namespace siren::dataclasses; + +namespace { + +// Build an unfinalized D0 -> {D0, Hadrons} record at a given lab energy along z. +InteractionRecord make_d0_record(const InteractionSignature & sig, double E_D) { + double mD = Constants::D0Mass; + double p_D = std::sqrt(E_D * E_D - mD * mD); + InteractionRecord rec; + rec.signature = sig; + rec.primary_mass = mD; + rec.primary_momentum = {E_D, 0, 0, p_D}; + rec.primary_helicity = 0; + rec.target_mass = Constants::protonMass; + rec.target_helicity = 0; + rec.secondary_momenta = {{0, 0, 0, 0}, {0, 0, 0, 0}}; + rec.secondary_masses = {mD, 0.0}; + rec.secondary_helicities = {0, 0}; + return rec; +} + +} // namespace + +// --- Test 1: the D meson always loses energy -------------------------------- + +TEST(DMesonELoss, EnergyMonotonicallyDecreases) { + DMesonELoss xs; + auto sigs = xs.GetPossibleSignaturesFromParents(ParticleType::D0, ParticleType::PPlus); + ASSERT_EQ(sigs.size(), 1u); + auto sig = sigs[0]; + EXPECT_EQ(sig.secondary_types[0], ParticleType::D0); + EXPECT_EQ(sig.secondary_types[1], ParticleType::Hadrons); + + double mD = Constants::D0Mass; + auto rng = std::make_shared(); + + for (double E_D : {10.0, 100.0, 1000.0}) { + for (int i = 0; i < 2000; ++i) { + InteractionRecord rec = make_d0_record(sig, E_D); + CrossSectionDistributionRecord cdr(rec); + xs.SampleFinalState(cdr, rng); + cdr.Finalize(rec); + + double E_out = rec.secondary_momenta[0][0]; + // The D meson never gains energy (z >= z_min_ > 0). + EXPECT_LT(E_out, E_D); + // It stays on (or above) its mass shell -- no z>z_max_ leakage. + EXPECT_GE(E_out, mD); + EXPECT_GT(E_out, 0.0); + } + } +} + +// --- Test 2: on-shell, energy conservation, collinearity -------------------- + +TEST(DMesonELoss, FourMomentumAndMassInvariants) { + DMesonELoss xs; + auto sig = xs.GetPossibleSignaturesFromParents(ParticleType::D0, ParticleType::PPlus)[0]; + double mD = Constants::D0Mass; + double E_D = 100.0; + double p_D = std::sqrt(E_D * E_D - mD * mD); + auto rng = std::make_shared(); + + for (int i = 0; i < 3000; ++i) { + InteractionRecord rec = make_d0_record(sig, E_D); + CrossSectionDistributionRecord cdr(rec); + xs.SampleFinalState(cdr, rng); + cdr.Finalize(rec); + + const auto & pdm = rec.secondary_momenta[0]; // outgoing D + const auto & ph = rec.secondary_momenta[1]; // hadron + + // (a) outgoing D is on its mass shell. + double m2 = pdm[0]*pdm[0] - pdm[1]*pdm[1] - pdm[2]*pdm[2] - pdm[3]*pdm[3]; + EXPECT_NEAR(std::sqrt(std::max(0.0, m2)), mD, 1e-6); + + // (b) energy is conserved: E0 == E_D_out + E_H. + EXPECT_NEAR(pdm[0] + ph[0], E_D, 1e-6); + // hadron carries the lost energy and is (nearly) massless. + EXPECT_GT(ph[0], 0.0); + double mh2 = ph[0]*ph[0] - ph[1]*ph[1] - ph[2]*ph[2] - ph[3]*ph[3]; + EXPECT_NEAR(mh2, 0.0, 1e-6); + + // (c) collinearity: both outgoing 3-momenta lie along +z (the incoming + // direction). Momentum is NOT conserved by design (massless hadron), + // so we assert direction, not p-balance. + EXPECT_NEAR(pdm[1], 0.0, 1e-9); + EXPECT_NEAR(pdm[2], 0.0, 1e-9); + EXPECT_GT(pdm[3], 0.0); + EXPECT_NEAR(ph[1], 0.0, 1e-9); + EXPECT_NEAR(ph[2], 0.0, 1e-9); + EXPECT_GT(ph[3], 0.0); + // direction of p_D matches the incoming +z direction. + EXPECT_GT(p_D, 0.0); + } +} + +// --- Test 3: total cross section positivity / monotonicity / throws --------- + +TEST(DMesonELoss, TotalCrossSectionPositiveAndThrows) { + DMesonELoss xs; + // Positive and increasing with energy over the (>1 PeV) validity range. + double last = -1.0; + for (double E : {1e6, 1e7, 1e8, 1e9}) { + double s = xs.TotalCrossSection(ParticleType::D0, E); + EXPECT_GT(s, 0.0); + if (last >= 0.0) EXPECT_GT(s, last); + last = s; + } + // Unsupported primary throws. + EXPECT_THROW(xs.TotalCrossSection(ParticleType::PiPlus, 1e7), std::runtime_error); +} + +// --- Test 4: sub-threshold primary fails recoverably ------------------------ + +TEST(DMesonELoss, SubThresholdThrows) { + DMesonELoss xs; + auto sig = xs.GetPossibleSignaturesFromParents(ParticleType::D0, ParticleType::PPlus)[0]; + auto rng = std::make_shared(); + double mD = Constants::D0Mass; + + // Primary energy exactly at the D mass (D at rest): no valid final state. + { + InteractionRecord rec; + rec.signature = sig; + rec.primary_mass = mD; + rec.primary_momentum = {mD, 0, 0, 0}; + rec.secondary_momenta = {{0, 0, 0, 0}, {0, 0, 0, 0}}; + rec.secondary_masses = {mD, 0.0}; + rec.secondary_helicities = {0, 0}; + CrossSectionDistributionRecord cdr(rec); + EXPECT_THROW(xs.SampleFinalState(cdr, rng), siren::utilities::InjectionFailure); + } + // Primary energy below the D mass. + { + double E_D = 0.5 * mD; + InteractionRecord rec; + rec.signature = sig; + rec.primary_mass = mD; + rec.primary_momentum = {E_D, 0, 0, 0}; + rec.secondary_momenta = {{0, 0, 0, 0}, {0, 0, 0, 0}}; + rec.secondary_masses = {mD, 0.0}; + rec.secondary_helicities = {0, 0}; + CrossSectionDistributionRecord cdr(rec); + EXPECT_THROW(xs.SampleFinalState(cdr, rng), siren::utilities::InjectionFailure); + } +} + +// --- Test 5: sampled-z density matches FinalStateProbability ---------------- + +TEST(DMesonELoss, ZDensityClosure) { + DMesonELoss xs; + auto sig = xs.GetPossibleSignaturesFromParents(ParticleType::D0, ParticleType::PPlus)[0]; + double E_D = 1000.0; // well above threshold, so the truncation is the only cut + auto rng = std::make_shared(); + + // FinalStateProbability returns dxs/txs == normalized truncated Gaussian in + // z over [z_min_, z_max_]. Histogram z = 1 - E_out/E_D and compare the + // empirical pdf to FinalStateProbability evaluated at each bin center. + const double zlo = 0.001, zhi = 0.999; + const int NB = 20; + const double bw = (zhi - zlo) / NB; + std::vector counts(NB, 0); + const int N = 200000; + + for (int i = 0; i < N; ++i) { + InteractionRecord rec = make_d0_record(sig, E_D); + CrossSectionDistributionRecord cdr(rec); + xs.SampleFinalState(cdr, rng); + cdr.Finalize(rec); + double E_out = rec.secondary_momenta[0][0]; + double z = 1.0 - E_out / E_D; + // All accepted samples lie in [z_min_, z_max_]. + EXPECT_GE(z, zlo - 1e-9); + EXPECT_LE(z, zhi + 1e-9); + int b = (int)((z - zlo) / bw); + if (b < 0) b = 0; + if (b >= NB) b = NB - 1; + counts[b]++; + } + + // Build a record at a chosen z and read back the normalized z-density. + auto fsp_at_z = [&](double z) -> double { + double E_out = E_D * (1.0 - z); + double mD = Constants::D0Mass; + double p_out = std::sqrt(std::max(0.0, E_out * E_out - mD * mD)); + InteractionRecord r; + r.signature = sig; + r.primary_mass = mD; + r.primary_momentum = {E_D, 0, 0, std::sqrt(E_D * E_D - mD * mD)}; + r.target_mass = Constants::protonMass; + r.secondary_momenta = {{E_out, 0, 0, p_out}, {E_D - E_out, 0, 0, E_D - E_out}}; + r.secondary_masses = {mD, 0.0}; + // FinalStateProbability = DifferentialCrossSection / TotalCrossSection, + // i.e. the normalized truncated Gaussian density in z. + return xs.FinalStateProbability(r); + }; + + // (a) The density is non-negative everywhere and the empirical pdf closes + // bin-by-bin within ~4 Poisson sigma. + for (int b = 0; b < NB; ++b) { + double zc = zlo + (b + 0.5) * bw; + double pred = fsp_at_z(zc); + EXPECT_GE(pred, 0.0); + if (counts[b] < 50) continue; + double emp = (double)counts[b] / (N * bw); + double sigma = std::sqrt((double)counts[b]) / (N * bw); + EXPECT_NEAR(emp, pred, 4.0 * sigma + 0.02 * pred); + } + + // (b) The density integrates to 1 over [z_min_, z_max_] (trapezoid on a + // fine grid) -- it is a properly normalized pdf in z. + int M = 2000; + double integral = 0.0; + double dz = (zhi - zlo) / M; + for (int i = 0; i <= M; ++i) { + double z = zlo + i * dz; + double w = (i == 0 || i == M) ? 0.5 : 1.0; + integral += w * fsp_at_z(z) * dz; + } + EXPECT_NEAR(integral, 1.0, 2e-2); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/projects/interactions/private/test/QuarkDISDensityContract_TEST.cxx b/projects/interactions/private/test/QuarkDISDensityContract_TEST.cxx new file mode 100644 index 000000000..bd9dfeb3b --- /dev/null +++ b/projects/interactions/private/test/QuarkDISDensityContract_TEST.cxx @@ -0,0 +1,78 @@ +/** + * Contract test for QuarkDISFromSpline (slow-rescaling charm DIS sampler). + * + * P6 finding: SampleFinalState samples (xi, y) AND an independently-sampled + * fragmentation z and a uniform azimuth phi that set the D-meson momentum, but + * the advertised density (DensityVariables / FinalStateProbability / + * DifferentialCrossSection) accounts for (xi, y) only. The omitted z/phi factors + * cancel in the weight ratio ONLY in the standard unbiased configuration (the + * same cross-section object supplies both the injection and physical densities + * and no biased phase-space channel is installed on the D kinematics). + * + * These tests do NOT require a spline file: the no-arg ctor only calls + * normalize_pdf() and compute_cdf() for the fragmentation pdf. + * + * Tests: + * 1. ContractPinsTwoDensityVariables -- DensityVariables() must contain exactly + * {"Bjorken xi", "Bjorken y"}. This is an intentional TRIPWIRE: if a future + * change adds the fragmentation-z (or phi) factor to the density, this test + * MUST be updated in lockstep, which forces the closure implications to be + * reconsidered (the z/phi factors then no longer simply cancel). + * 2. FragmentationPdfNormalized -- the fragmentation pdf sample_pdf(z) is a + * properly normalized density over (0.001, 0.999): its integral is 1. This + * documents that the z density EXISTS and is normalized, i.e. the + * alternative (carry z in the density) path is feasible. + */ +#include +#include +#include + +#include + +#include "SIREN/interactions/QuarkDISFromSpline.h" + +using namespace siren::interactions; + +// --- Test 1: density-variable contract (tripwire) --------------------------- + +TEST(QuarkDISDensityContract, ContractPinsTwoDensityVariables) { + // The no-arg ctor builds only the fragmentation pdf/cdf -- no spline needed. + QuarkDISFromSpline xs; + std::vector vars = xs.DensityVariables(); + + // Tripwire: the density covers exactly (xi, y). The independently-sampled + // fragmentation z and azimuth phi are deliberately NOT in the density. If + // this assertion ever needs to change, the closure argument (z/phi cancel in + // the weight ratio) must be re-derived -- do not simply bump the count. + ASSERT_EQ(vars.size(), 2u); + EXPECT_EQ(vars[0], "Bjorken xi"); + EXPECT_EQ(vars[1], "Bjorken y"); +} + +// --- Test 2: fragmentation pdf is normalized -------------------------------- + +TEST(QuarkDISDensityContract, FragmentationPdfNormalized) { + QuarkDISFromSpline xs; + + // sample_pdf(z) divides the unnormalized fragmentation integrand by + // fragmentation_integral (the integral of that integrand over [0.001,0.999]), + // so it must integrate to 1 over the same interval. Composite-trapezoid on a + // fine grid; the integrand is smooth and bounded away from the z=0,1 poles. + const double zlo = 0.001, zhi = 0.999; + const int M = 20000; + const double dz = (zhi - zlo) / M; + double integral = 0.0; + for (int i = 0; i <= M; ++i) { + double z = zlo + i * dz; + double w = (i == 0 || i == M) ? 0.5 : 1.0; + double f = xs.sample_pdf(z); + EXPECT_GE(f, 0.0); // a pdf is non-negative on its support + integral += w * f * dz; + } + EXPECT_NEAR(integral, 1.0, 1e-3); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/projects/math/private/test/Vector3D_TEST.cxx b/projects/math/private/test/Vector3D_TEST.cxx index b0abc7da7..4eb18f798 100644 --- a/projects/math/private/test/Vector3D_TEST.cxx +++ b/projects/math/private/test/Vector3D_TEST.cxx @@ -218,6 +218,65 @@ TEST(Normalize, Operator) EXPECT_TRUE(B != C); } +// Regression test for the degenerate trajectory-direction fix (commit 29e189f6). +// Before the fix, Vector3D::normalize() divided every component by a zero length +// for a zero vector, producing NaN. This is the root cause that propagated up +// into DetectorModel where a degenerate (p1 - p0) direction was normalized and +// then asserted to be unit length. The guard now leaves a zero vector unchanged. +TEST(Normalize, ZeroVectorDoesNotProduceNaN) +{ + Vector3D Z; + Z.SetCartesianCoordinates(0.0, 0.0, 0.0); + // Must not abort and must not produce NaN/Inf. + EXPECT_NO_THROW(Z.normalize()); + std::array z = Z; + EXPECT_FALSE(std::isnan(z[0])); + EXPECT_FALSE(std::isnan(z[1])); + EXPECT_FALSE(std::isnan(z[2])); + EXPECT_TRUE(std::isfinite(z[0])); + EXPECT_TRUE(std::isfinite(z[1])); + EXPECT_TRUE(std::isfinite(z[2])); + // The zero vector is left unchanged (magnitude stays exactly 0). + EXPECT_DOUBLE_EQ(0.0, z[0]); + EXPECT_DOUBLE_EQ(0.0, z[1]); + EXPECT_DOUBLE_EQ(0.0, z[2]); + EXPECT_DOUBLE_EQ(0.0, Z.magnitude()); +} + +// normalized() is the const sibling and must also be NaN-safe on a zero vector. +TEST(Normalize, ZeroVectorNormalizedIsFinite) +{ + Vector3D Z; + Z.SetCartesianCoordinates(0.0, 0.0, 0.0); + Vector3D N = Z.normalized(); + std::array n = N; + EXPECT_FALSE(std::isnan(n[0]) || std::isnan(n[1]) || std::isnan(n[2])); + EXPECT_DOUBLE_EQ(0.0, N.magnitude()); +} + +// A near-zero but genuinely tiny difference of two Earth-scale coordinates (the +// pattern that arises from p1 - p0 with catastrophic cancellation) must still +// normalize to a finite, unit-length vector when the difference is nonzero. +TEST(Normalize, TinyEarthScaleDifferenceIsFiniteUnit) +{ + // Two points separated by 1e-7 m built at Earth-scale x ~ 6.371e6 m. + // The subtraction is exact here (1e-7 is representable relative to 6.371e6), + // so the magnitude is nonzero and normalize() must yield a unit vector. + Vector3D p0; + p0.SetCartesianCoordinates(6.371e6, 0.0, 0.0); + Vector3D p1; + p1.SetCartesianCoordinates(6.371e6 + 1e-7, 0.0, 0.0); + Vector3D direction = p1 - p0; + double mag = direction.magnitude(); + ASSERT_GT(mag, 0.0); + direction.normalize(); + std::array d = direction; + EXPECT_FALSE(std::isnan(d[0]) || std::isnan(d[1]) || std::isnan(d[2])); + // Resulting vector is unit length. + EXPECT_NEAR(1.0, direction.magnitude(), 1e-12); + EXPECT_NEAR(1.0, d[0], 1e-12); +} + TEST(CalculateSphericalCoordinates, Conversion) { Vector3D A; diff --git a/tests/python/test_quarkdis_slow_rescaling.py b/tests/python/test_quarkdis_slow_rescaling.py new file mode 100644 index 000000000..bf263dd4c --- /dev/null +++ b/tests/python/test_quarkdis_slow_rescaling.py @@ -0,0 +1,364 @@ +"""Slow-rescaling (xi, y) sampling tests for QuarkDISFromSpline. + +These were previously two orphan smoke scripts under tests/slow_rescaling/ +(smoke_quarkdis_100.py, smoke_quarkdis_10k.py) that were never collected by +pytest (testpaths = tests/python) and aborted the session with module-level +sys.exit() calls. They are now real, collectable pytest tests. + +The charm slow-rescaling FITS splines are LHAPDF-derived and not committed to +the repository, so every spline-dependent test is gated behind the +SIREN_CHARM_SPLINE_DIR environment variable. Point it at a directory containing + + dsdxidy_nu-N-cc-charm-CT14nlo_central.fits + sigma_nu-N-cc-charm-CT14nlo_central.fits + +(e.g. the FASRC Maboi_M_Muon_SR spline set) to run the physics asserts. When the +variable is unset, or the FITS files are absent, the whole module skips cleanly. + +Number of events for the differential-positivity test is configurable via +SIREN_CHARM_NEVENTS (default 2000, kept modest so the suite stays fast; set it +to 10000 to reproduce the original 10k smoke run). +""" +import math +import os + +import pytest + +siren = pytest.importorskip("siren") + +# --------------------------------------------------------------------------- +# Constants (mirror C++ siren::utilities::Constants values exactly) +# --------------------------------------------------------------------------- +M_C = 1.27 # Constants::charmMass +M_D0 = 1.86484 # Constants::D0Mass +M_N = (0.938272 + 0.939565) / 2 # isoscalar nucleon mass +M_MU = 0.105658 # muon mass +Q2MIN = 1.0 # GeV^2 +E_NU = 100.0 # neutrino energy in GeV + +# Number of kinematic-bound events (the cheap test) and re-evaluation events +# (the heavier differential-positivity test). +N_BOUNDS = 100 +N_DIFF = int(os.environ.get("SIREN_CHARM_NEVENTS", "2000")) + +# Per-event resample budget when the sampler raises a recoverable +# InjectionFailure (surfaces in Python as RuntimeError). The production injector +# framework retries automatically; a direct SampleFinalState caller must do so. +MAX_RETRIES = 100 + +# --------------------------------------------------------------------------- +# Spline-file gating (resolves the old hardcoded /n/holylfs05 cluster path) +# --------------------------------------------------------------------------- +_SPLINE_DIR = os.environ.get("SIREN_CHARM_SPLINE_DIR") +_DIFF_FILE = ( + os.path.join(_SPLINE_DIR, "dsdxidy_nu-N-cc-charm-CT14nlo_central.fits") + if _SPLINE_DIR + else None +) +_TOTAL_FILE = ( + os.path.join(_SPLINE_DIR, "sigma_nu-N-cc-charm-CT14nlo_central.fits") + if _SPLINE_DIR + else None +) +_have_splines = bool( + _SPLINE_DIR + and _DIFF_FILE + and _TOTAL_FILE + and os.path.exists(_DIFF_FILE) + and os.path.exists(_TOTAL_FILE) +) + +pytestmark = pytest.mark.skipif( + not _have_splines, + reason=( + "set SIREN_CHARM_SPLINE_DIR to a directory containing " + "dsdxidy_nu-N-cc-charm-*.fits and sigma_nu-N-cc-charm-*.fits " + "to run the charm slow-rescaling tests" + ), +) + + +# --------------------------------------------------------------------------- +# Expected kinematic bounds (mirror C++ SampleFinalState sampling bounds) +# --------------------------------------------------------------------------- +Y_MAX = 1.0 - M_MU / E_NU +W2_THR = (M_N + M_D0) ** 2 +Y_MIN = (W2_THR - M_N ** 2 + Q2MIN) / (2.0 * M_N * E_NU) +XI_MIN = (M_C ** 2 + Q2MIN) / (2.0 * M_N * E_NU * Y_MAX) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- +@pytest.fixture(scope="module") +def charm_xs(): + """Build the QuarkDISFromSpline cross section once for the module.""" + import siren.interactions + import siren.dataclasses + + PT = siren.dataclasses.Particle.ParticleType + xs = siren.interactions.QuarkDISFromSpline( + _DIFF_FILE, + _TOTAL_FILE, + int(1), # interaction_type: CC + M_N, # isoscalar target mass + int(1), # minimum Q2 (GeV^2) + [PT.NuMu], # primary types + [PT.O16Nucleus], # target types + "m", # units + ) + return xs + + +@pytest.fixture(scope="module") +def signature(charm_xs): + sigs = list(charm_xs.GetPossibleSignatures()) + assert len(sigs) > 0, "QuarkDISFromSpline returned no signatures" + return sigs[0] + + +@pytest.fixture +def rng(): + import siren.utilities + return siren.utilities.SIREN_random(1234) + + +def _make_neutrino_record(sig): + """A fresh input InteractionRecord for a 100 GeV nu_mu along +z.""" + import siren.dataclasses + ir = siren.dataclasses.InteractionRecord() + ir.signature = sig + ir.primary_momentum = [E_NU, 0.0, 0.0, E_NU] # massless nu along +z + ir.primary_mass = 0.0 + ir.target_mass = M_N + return ir + + +def _sample_with_retries(charm_xs, sig, rng): + """Sample one event, retrying on recoverable InjectionFailure. + + Returns the populated CrossSectionDistributionRecord, or None if the + per-event resample budget was exhausted (an expected, rare NaN-guard + rejection, not a test failure). InjectionFailure derives from + std::runtime_error, so it surfaces in Python as RuntimeError. + """ + import siren.dataclasses + ir = _make_neutrino_record(sig) + for retry in range(MAX_RETRIES + 1): + cdr = siren.dataclasses.CrossSectionDistributionRecord(ir) + try: + charm_xs.SampleFinalState(cdr, rng) + return cdr + except RuntimeError: + # Recoverable per-event sampling failure; resample with fresh + # RNG draws. Hard configuration errors (bad signature, units, or + # spline) would already have fired in the fixtures, not here. + if retry < MAX_RETRIES: + continue + return None + return None + + +# --------------------------------------------------------------------------- +# Test 1: kinematic bounds on the sampled (xi, y) over 100 events +# --------------------------------------------------------------------------- +def test_quarkdis_kinematic_bounds(charm_xs, signature, rng): + """Every sampled event must satisfy the slow-rescaling kinematic bounds.""" + assert XI_MIN < 1.0, "degenerate bounds: xi_min >= 1" + assert Y_MIN < Y_MAX, "degenerate bounds: y_min >= y_max" + + failures = [] + rejected = 0 + sampled = 0 + + for event_idx in range(N_BOUNDS): + cdr = _sample_with_retries(charm_xs, signature, rng) + if cdr is None: + rejected += 1 + continue + sampled += 1 + + params = dict(cdr.interaction_parameters) + xi = params["bjorken_xi"] + y = params["bjorken_y"] + x = params["bjorken_x"] + + # Derived quantities (mirror C++ slow-rescaling relations). + Q2 = 2.0 * M_N * E_NU * y * xi - M_C ** 2 + W2 = M_N ** 2 + 2.0 * M_N * E_NU * y * (1.0 - xi) + M_C ** 2 + + event_failures = [] + if not (XI_MIN <= xi <= 1.0): + event_failures.append(f"xi={xi:.6g} not in [{XI_MIN:.6g}, 1.0]") + if not (Y_MIN <= y <= Y_MAX): + event_failures.append( + f"y={y:.6g} not in [{Y_MIN:.6g}, {Y_MAX:.6g}]" + ) + if not (x > 0.0): + event_failures.append(f"x={x:.6g} not > 0") + if not (Q2 >= Q2MIN): + event_failures.append(f"Q2={Q2:.6g} < Q2MIN={Q2MIN:.6g}") + if not (W2 > W2_THR): + event_failures.append(f"W2={W2:.6g} not > W2_thr={W2_THR:.6g}") + + if event_failures: + failures.append( + f"event {event_idx}: xi={xi:.6g} y={y:.6g} x={x:.6g} " + f"Q2={Q2:.6g} W2={W2:.6g} | " + "; ".join(event_failures) + ) + + # The vast majority of slots must produce a sample; a few NaN-guard + # rejections are tolerated but a wholesale failure indicates a real bug. + assert sampled > 0, "no events were sampled at all" + assert rejected <= N_BOUNDS // 10, ( + f"{rejected}/{N_BOUNDS} events were unrecoverable " + f"(expected at most {N_BOUNDS // 10})" + ) + assert not failures, ( + f"{len(failures)} of {sampled} sampled events violated kinematic " + "bounds:\n" + "\n".join(failures[:10]) + ) + + +# --------------------------------------------------------------------------- +# Test 2: differential cross section is finite-positive on the production path +# --------------------------------------------------------------------------- +def test_quarkdis_differential_positive(charm_xs, signature, rng): + """Re-evaluate the spline on finalized records via the production path. + + This drives the REAL weighting density: SampleFinalState populates the + CrossSectionDistributionRecord, cdr.finalize(ir_out) materializes the + secondary momenta, and DifferentialCrossSection(ir_out) is evaluated on the + finalized record. That exercises the primary-momentum Q2 branch of + DifferentialCrossSection -- exactly the path the Weighter runs -- instead + of the deliberately-broken zero-momenta fallback used by the old smoke + script. We assert a high finite-positive fraction. + """ + import siren.dataclasses + + positive = [] + nonpositive = 0 + eval_errors = 0 + rejected = 0 + sampled = 0 + + n_secondaries = len(signature.secondary_types) + + for event_idx in range(N_DIFF): + cdr = _sample_with_retries(charm_xs, signature, rng) + if cdr is None: + rejected += 1 + continue + sampled += 1 + + # Production path: materialize the sampled state into an output record. + ir_out = siren.dataclasses.InteractionRecord() + ir_out.signature = signature + ir_out.primary_momentum = [E_NU, 0.0, 0.0, E_NU] + ir_out.primary_mass = 0.0 + cdr.finalize(ir_out) + + # finalize must populate the secondary momenta from the sampled state. + assert len(ir_out.secondary_momenta) == n_secondaries, ( + f"event {event_idx}: finalize wrote " + f"{len(ir_out.secondary_momenta)} secondary momenta, " + f"expected {n_secondaries}" + ) + + try: + val = charm_xs.DifferentialCrossSection(ir_out) + except Exception: + eval_errors += 1 + continue + + if math.isfinite(val) and val > 0.0: + positive.append(val) + else: + nonpositive += 1 + + assert sampled > 0, "no events were sampled at all" + assert eval_errors == 0, ( + f"{eval_errors} of {sampled} DifferentialCrossSection evaluations " + "raised on the production (finalized) path" + ) + + positive_fraction = len(positive) / sampled + assert positive_fraction > 0.95, ( + f"only {positive_fraction:.4f} of {sampled} finalized records had a " + f"finite-positive DifferentialCrossSection ({nonpositive} non-positive)" + ) + + mean_log_xs = sum(math.log10(v) for v in positive) / len(positive) + assert math.isfinite(mean_log_xs), ( + f"mean log10 differential cross section is not finite: {mean_log_xs}" + ) + + +# --------------------------------------------------------------------------- +# Test 3: Sample == Density closure on the finalized record +# --------------------------------------------------------------------------- +def test_quarkdis_sample_density_closure(charm_xs, signature, rng): + """The two Q2 derivations in DifferentialCrossSection must agree. + + DifferentialCrossSection(InteractionRecord) takes the primary-momentum Q2 + branch when the finalized record carries real secondary momenta, and the + stored-(xi, y) fallback branch when momenta are absent. Both must yield the + same differential value for a self-consistently sampled event; FinalState- + Probability (= dxs/txs, the physical density the Weighter uses) must be + finite and non-negative. + """ + import siren.dataclasses + + checked = 0 + for event_idx in range(N_BOUNDS): + cdr = _sample_with_retries(charm_xs, signature, rng) + if cdr is None: + continue + + params = dict(cdr.interaction_parameters) + + # Finalized record -> primary-momentum Q2 branch. + ir_out = siren.dataclasses.InteractionRecord() + ir_out.signature = signature + ir_out.primary_momentum = [E_NU, 0.0, 0.0, E_NU] + ir_out.primary_mass = 0.0 + cdr.finalize(ir_out) + dxs_primary = charm_xs.DifferentialCrossSection(ir_out) + + if not (math.isfinite(dxs_primary) and dxs_primary > 0.0): + # Skip rare edge points where the spline returns 0; closure is only + # meaningful where the differential value is positive on both paths. + continue + + # Same record stripped of secondary momenta -> stored-(xi, y) fallback. + ir_fb = siren.dataclasses.InteractionRecord() + ir_fb.signature = signature + ir_fb.primary_momentum = [E_NU, 0.0, 0.0, E_NU] + ir_fb.primary_mass = 0.0 + ir_fb.target_mass = M_N + ir_fb.secondary_momenta = [[0.0, 0.0, 0.0, 0.0]] * len(signature.secondary_types) + ir_fb.secondary_masses = [0.0] * len(signature.secondary_types) + ir_fb.interaction_parameters = { + "energy": E_NU, + "bjorken_xi": params["bjorken_xi"], + "bjorken_y": params["bjorken_y"], + } + dxs_fallback = charm_xs.DifferentialCrossSection(ir_fb) + + # Both branches read the same spline at the same (E, xi, y); they should + # agree to within float roundoff in the Q2 derivation. + rel = abs(dxs_primary - dxs_fallback) / max(dxs_primary, dxs_fallback) + assert rel < 1e-3, ( + f"event {event_idx}: primary-path dxs={dxs_primary:.6g} disagrees " + f"with fallback dxs={dxs_fallback:.6g} (rel={rel:.3g}); the two Q2 " + "derivations in DifferentialCrossSection are inconsistent" + ) + + # Physical density used by the Weighter. + fsp = charm_xs.FinalStateProbability(ir_out) + assert math.isfinite(fsp), f"event {event_idx}: FinalStateProbability not finite: {fsp}" + assert fsp >= 0.0, f"event {event_idx}: FinalStateProbability negative: {fsp}" + + checked += 1 + + assert checked > 0, "no positive-dxs events were available for closure check" diff --git a/tests/slow_rescaling/smoke_quarkdis_100.py b/tests/slow_rescaling/smoke_quarkdis_100.py deleted file mode 100644 index 14164966f..000000000 --- a/tests/slow_rescaling/smoke_quarkdis_100.py +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env python3 -""" -Smoke test for slow-rescaling (xi, y) sampling in QuarkDISFromSpline. -Exercises 100 events and verifies kinematic bounds for each. -""" -import sys -import traceback - -# --------------------------------------------------------------------------- -# Constants (mirror C++ values exactly) -# --------------------------------------------------------------------------- -M_C = 1.27 # Constants::CharmMass -M_D0 = 1.86484 # Constants::D0Mass -M_N = (0.938272 + 0.939565) / 2 # isoscalar nucleon mass -M_MU = 0.105658 # muon mass -Q2MIN = 1.0 # GeV^2 -N_EVENTS = 100 -E_NU = 100.0 # neutrino energy in GeV - -# Spline files -SPLINE_DIR = ( - "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/pzhelnin/" - "DiMuons/Simulation/Resources/Splines/Maboi_M_Muon_SR" -) -DIFF_FILE = SPLINE_DIR + "/dsdxidy_nu-N-cc-charm-CT14nlo_central.fits" -TOTAL_FILE = SPLINE_DIR + "/sigma_nu-N-cc-charm-CT14nlo_central.fits" - -# --------------------------------------------------------------------------- -# Import SIREN -# --------------------------------------------------------------------------- -try: - import siren - import siren.interactions - import siren.dataclasses - import siren.utilities -except Exception as exc: - print(f"IMPORT ERROR: {exc}", file=sys.stderr) - traceback.print_exc(file=sys.stderr) - sys.exit(1) - -PT = siren.dataclasses.Particle.ParticleType - -# --------------------------------------------------------------------------- -# Construct cross-section object -# --------------------------------------------------------------------------- -try: - xs = siren.interactions.QuarkDISFromSpline( - DIFF_FILE, - TOTAL_FILE, - int(1), # interaction_type: CC - M_N, # isoscalar mass - int(1), # minimum Q2 - [PT.NuMu], # primary types - [PT.O16Nucleus], # target types - "m", # units - ) -except Exception as exc: - print(f"CONSTRUCTOR ERROR: {exc}", file=sys.stderr) - traceback.print_exc(file=sys.stderr) - sys.exit(1) - -# --------------------------------------------------------------------------- -# Get a valid signature -# --------------------------------------------------------------------------- -try: - sigs = list(xs.GetPossibleSignatures()) - assert len(sigs) > 0, "QuarkDISFromSpline returned no signatures" - sig = sigs[0] -except Exception as exc: - print(f"SIGNATURE ERROR: {exc}", file=sys.stderr) - traceback.print_exc(file=sys.stderr) - sys.exit(1) - -# --------------------------------------------------------------------------- -# RNG -# --------------------------------------------------------------------------- -rng = siren.utilities.SIREN_random(1234) - -# --------------------------------------------------------------------------- -# Expected kinematic bounds (mirror C++ Step 5) -# --------------------------------------------------------------------------- -y_max = 1.0 - M_MU / E_NU -W2_thr = (M_N + M_D0) ** 2 -y_min = (W2_thr - M_N**2 + Q2MIN) / (2.0 * M_N * E_NU) -xi_min = (M_C**2 + Q2MIN) / (2.0 * M_N * E_NU * y_max) - -print(f"Expected bounds:") -print(f" y_min = {y_min:.6f}") -print(f" y_max = {y_max:.6f}") -print(f" xi_min = {xi_min:.6f}") -print(f" W2_thr = {W2_thr:.6f}") -print(f" Q2MIN = {Q2MIN:.6f}") -print() - -# --------------------------------------------------------------------------- -# Run N_EVENTS events -# --------------------------------------------------------------------------- -failures = [] -rejected = [] -total_retries = 0 -MAX_RETRIES = 100 - -for event_idx in range(N_EVENTS): - try: - ir = siren.dataclasses.InteractionRecord() - ir.signature = sig - ir.primary_momentum = [E_NU, 0.0, 0.0, E_NU] # massless nu along +z - ir.primary_mass = 0.0 - ir.target_mass = M_N - - # Retry on NaN-guard InjectionFailure (mirrors SIREN Injector behavior). - for retry in range(MAX_RETRIES + 1): - cdr = siren.dataclasses.CrossSectionDistributionRecord(ir) - try: - xs.SampleFinalState(cdr, rng) - if retry > 0: - total_retries += retry - break - except RuntimeError as exc: - if 'precision loop failed to converge' in str(exc): - if retry < MAX_RETRIES: - continue - rejected.append(event_idx) - raise - raise - - params = dict(cdr.interaction_parameters) - xi = params["bjorken_xi"] - y = params["bjorken_y"] - x = params["bjorken_x"] - - # Derived quantities - Q2 = 2.0 * M_N * E_NU * y * xi - M_C**2 - W2 = M_N**2 + 2.0 * M_N * E_NU * y * (1.0 - xi) + M_C**2 - - # Check all assertions - event_failures = [] - if not (xi_min <= xi <= 1.0): - event_failures.append( - f"xi={xi:.6g} not in [{xi_min:.6g}, 1.0]" - ) - if not (y_min <= y <= y_max): - event_failures.append( - f"y={y:.6g} not in [{y_min:.6g}, {y_max:.6g}]" - ) - if not (x > 0.0): - event_failures.append(f"x={x:.6g} not > 0") - if not (Q2 >= Q2MIN): - event_failures.append( - f"Q2={Q2:.6g} < Q2MIN={Q2MIN:.6g}" - ) - if not (W2 > W2_thr): - event_failures.append( - f"W2={W2:.6g} not > W2_thr={W2_thr:.6g}" - ) - - if event_failures: - msg = ( - f"Event {event_idx}: xi={xi:.6g} y={y:.6g} " - f"x={x:.6g} Q2={Q2:.6g} W2={W2:.6g} | " - + "; ".join(event_failures) - ) - failures.append(msg) - print(f"FAIL: {msg}") - - except Exception as exc: - msg = f"Event {event_idx}: unexpected exception: {exc}" - failures.append(msg) - print(f"FAIL: {msg}", file=sys.stderr) - traceback.print_exc(file=sys.stderr) - - if len(failures) >= 10: - print(f"Stopping early after {len(failures)} failures.") - break - -# --------------------------------------------------------------------------- -# Report -# --------------------------------------------------------------------------- -if failures: - n_fail = len(failures) - n_ok = N_EVENTS - n_fail - print(f"\nFAIL {n_ok}/{N_EVENTS} ({n_fail} events failed bounds checks)") - sys.exit(1) -else: - retry_info = f" with {total_retries} resamples" if total_retries > 0 else "" - rej_info = f", {len(rejected)} unrecoverable" if rejected else "" - print(f"OK {N_EVENTS - len(rejected)}/{N_EVENTS} sampled{retry_info}{rej_info}") - sys.exit(0) diff --git a/tests/slow_rescaling/smoke_quarkdis_10k.py b/tests/slow_rescaling/smoke_quarkdis_10k.py deleted file mode 100644 index 3e25f7c4d..000000000 --- a/tests/slow_rescaling/smoke_quarkdis_10k.py +++ /dev/null @@ -1,295 +0,0 @@ -#!/usr/bin/env python3 -""" -Smoke test for slow-rescaling (xi, y) sampling in QuarkDISFromSpline. -Exercises 10000 events, verifies kinematic bounds for each, and -checks that the spline DifferentialCrossSection returns finite positive -values for >=95% of sampled events. - -Spline re-evaluation approach: fallback via explicit bjorken_xi/bjorken_y. -A fresh InteractionRecord is constructed with dummy (zero) secondary_momenta -so the C++ code reaches its fallback path (Q2=0 < Q2MIN triggers it), which -then reads xi and y from interaction_parameters. -""" -import sys -import math -import traceback - -# --------------------------------------------------------------------------- -# Constants (mirror C++ values exactly) -# --------------------------------------------------------------------------- -M_C = 1.27 # Constants::CharmMass -M_D0 = 1.86484 # Constants::D0Mass -M_N = (0.938272 + 0.939565) / 2 # isoscalar nucleon mass -M_MU = 0.105658 # muon mass -Q2MIN = 1.0 # GeV^2 -N_EVENTS = 10000 -E_NU = 100.0 # neutrino energy in GeV - -# Spline files -SPLINE_DIR = ( - "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/pzhelnin/" - "DiMuons/Simulation/Resources/Splines/Maboi_M_Muon_SR" -) -DIFF_FILE = SPLINE_DIR + "/dsdxidy_nu-N-cc-charm-CT14nlo_central.fits" -TOTAL_FILE = SPLINE_DIR + "/sigma_nu-N-cc-charm-CT14nlo_central.fits" - -# --------------------------------------------------------------------------- -# Import SIREN -# --------------------------------------------------------------------------- -try: - import siren - import siren.interactions - import siren.dataclasses - import siren.utilities -except Exception as exc: - print(f"IMPORT ERROR: {exc}", file=sys.stderr) - traceback.print_exc(file=sys.stderr) - sys.exit(1) - -PT = siren.dataclasses.Particle.ParticleType - -# --------------------------------------------------------------------------- -# Construct cross-section object -# --------------------------------------------------------------------------- -try: - xs = siren.interactions.QuarkDISFromSpline( - DIFF_FILE, - TOTAL_FILE, - int(1), # interaction_type: CC - M_N, # isoscalar mass - int(1), # minimum Q2 - [PT.NuMu], # primary types - [PT.O16Nucleus], # target types - "m", # units - ) -except Exception as exc: - print(f"CONSTRUCTOR ERROR: {exc}", file=sys.stderr) - traceback.print_exc(file=sys.stderr) - sys.exit(1) - -# --------------------------------------------------------------------------- -# Get a valid signature -# --------------------------------------------------------------------------- -try: - sigs = list(xs.GetPossibleSignatures()) - assert len(sigs) > 0, "QuarkDISFromSpline returned no signatures" - sig = sigs[0] - n_secondaries = len(sig.secondary_types) -except Exception as exc: - print(f"SIGNATURE ERROR: {exc}", file=sys.stderr) - traceback.print_exc(file=sys.stderr) - sys.exit(1) - -# --------------------------------------------------------------------------- -# RNG -# --------------------------------------------------------------------------- -rng = siren.utilities.SIREN_random(1234) - -# --------------------------------------------------------------------------- -# Expected kinematic bounds (mirror C++ Step 5) -# --------------------------------------------------------------------------- -y_max = 1.0 - M_MU / E_NU -W2_thr = (M_N + M_D0) ** 2 -y_min = (W2_thr - M_N**2 + Q2MIN) / (2.0 * M_N * E_NU) -xi_min = (M_C**2 + Q2MIN) / (2.0 * M_N * E_NU * y_max) - -print(f"Expected bounds:") -print(f" y_min = {y_min:.6f}") -print(f" y_max = {y_max:.6f}") -print(f" xi_min = {xi_min:.6f}") -print(f" W2_thr = {W2_thr:.6f}") -print(f" Q2MIN = {Q2MIN:.6f}") -print() - -# --------------------------------------------------------------------------- -# Run N_EVENTS events — kinematic checks + collect (xi, y) for spline eval -# --------------------------------------------------------------------------- -failures = [] -rejected = [] # NaN-guard rejections from precision loop (expected, not failures) -sampled_kinematics = [] # list of (event_idx, xi, y, x, Q2, W2) - -MAX_RETRIES = 100 # max resamples per event slot when NaN guard fires -total_retries = 0 - -for event_idx in range(N_EVENTS): - try: - ir = siren.dataclasses.InteractionRecord() - ir.signature = sig - ir.primary_momentum = [E_NU, 0.0, 0.0, E_NU] # massless nu along +z - ir.primary_mass = 0.0 - ir.target_mass = M_N - - # Retry loop mirrors SIREN Injector behavior: on InjectionFailure - # (precision-loop NaN guard), resample with new RNG state until success. - # Production injector framework retries automatically; direct - # SampleFinalState calls (like this smoke test) need to do it explicitly. - for retry in range(MAX_RETRIES + 1): - cdr = siren.dataclasses.CrossSectionDistributionRecord(ir) - try: - xs.SampleFinalState(cdr, rng) - if retry > 0: - total_retries += retry - break - except RuntimeError as exc: - if 'precision loop failed to converge' in str(exc): - if retry < MAX_RETRIES: - continue # retry with fresh RNG draws - rejected.append(event_idx) - raise # exhausted budget - raise - else: - rejected.append(event_idx) - raise RuntimeError(f'event {event_idx}: exhausted {MAX_RETRIES} retries') - - params = dict(cdr.interaction_parameters) - xi = params["bjorken_xi"] - y = params["bjorken_y"] - x = params["bjorken_x"] - - # Derived quantities - Q2 = 2.0 * M_N * E_NU * y * xi - M_C**2 - W2 = M_N**2 + 2.0 * M_N * E_NU * y * (1.0 - xi) + M_C**2 - - # Record kinematics for spline re-evaluation - sampled_kinematics.append((event_idx, xi, y, x, Q2, W2)) - - # Check all assertions - event_failures = [] - if not (xi_min <= xi <= 1.0): - event_failures.append( - f"xi={xi:.6g} not in [{xi_min:.6g}, 1.0]" - ) - if not (y_min <= y <= y_max): - event_failures.append( - f"y={y:.6g} not in [{y_min:.6g}, {y_max:.6g}]" - ) - if not (x > 0.0): - event_failures.append(f"x={x:.6g} not > 0") - if not (Q2 >= Q2MIN): - event_failures.append( - f"Q2={Q2:.6g} < Q2MIN={Q2MIN:.6g}" - ) - if not (W2 > W2_thr): - event_failures.append( - f"W2={W2:.6g} not > W2_thr={W2_thr:.6g}" - ) - - if event_failures: - msg = ( - f"Event {event_idx}: xi={xi:.6g} y={y:.6g} " - f"x={x:.6g} Q2={Q2:.6g} W2={W2:.6g} | " - + "; ".join(event_failures) - ) - failures.append(msg) - if len(failures) <= 10: - print(f"FAIL: {msg}") - - except RuntimeError as exc: - if 'precision loop failed to converge' in str(exc): - rejected.append(event_idx) - continue - msg = f"Event {event_idx}: unexpected exception: {exc}" - failures.append(msg) - if len(failures) <= 10: - print(f"FAIL: {msg}", file=sys.stderr) - traceback.print_exc(file=sys.stderr) - except Exception as exc: - msg = f"Event {event_idx}: unexpected exception: {exc}" - failures.append(msg) - if len(failures) <= 10: - print(f"FAIL: {msg}", file=sys.stderr) - traceback.print_exc(file=sys.stderr) - -# --------------------------------------------------------------------------- -# Spline re-evaluation: fallback approach -# -# Construct a fresh InteractionRecord with: -# - dummy zero secondary_momenta (so the C++ code computes Q2=0 < Q2MIN, -# which triggers its fallback path to read xi/y from interaction_parameters) -# - explicit bjorken_xi / bjorken_y in interaction_parameters -# -# Note: the "preferred" approach (cdr.record) crashes because secondary_momenta -# is not populated by SampleFinalState on CrossSectionDistributionRecord. -# --------------------------------------------------------------------------- -positive_xs_values = [] -zero_or_nan_count = 0 -xs_eval_errors = 0 - -dummy_momenta = [[0.0, 0.0, 0.0, 0.0]] * n_secondaries -dummy_masses = [0.0] * n_secondaries - -for (event_idx, xi, y, x, Q2, W2) in sampled_kinematics: - try: - ir2 = siren.dataclasses.InteractionRecord() - ir2.signature = sig - ir2.primary_momentum = [E_NU, 0.0, 0.0, E_NU] - ir2.primary_mass = 0.0 - ir2.target_mass = M_N - ir2.secondary_momenta = dummy_momenta - ir2.secondary_masses = dummy_masses - ir2.interaction_parameters = { - "energy": E_NU, - "bjorken_xi": xi, - "bjorken_y": y, - } - val = xs.DifferentialCrossSection(ir2) - if math.isfinite(val) and val > 0.0: - positive_xs_values.append(val) - else: - zero_or_nan_count += 1 - except Exception as exc: - xs_eval_errors += 1 - -# --------------------------------------------------------------------------- -# Summary statistics -# --------------------------------------------------------------------------- -n_evaluated = len(sampled_kinematics) -positive_fraction = len(positive_xs_values) / n_evaluated if n_evaluated > 0 else 0.0 - -if positive_xs_values: - mean_log_xs = sum(math.log10(v) for v in positive_xs_values) / len(positive_xs_values) -else: - mean_log_xs = float("nan") - -print(f"mean_log_xs = {mean_log_xs:.6f}") -print(f"positive xs fraction = {positive_fraction:.6f}") -if xs_eval_errors > 0 or zero_or_nan_count > 0: - print(f" (xs eval errors: {xs_eval_errors}, zero/nan: {zero_or_nan_count})") - -# --------------------------------------------------------------------------- -# Assertions -# --------------------------------------------------------------------------- -all_failures = [] - -if len(failures) != 0: - all_failures.append(f"kinematic failures: {len(failures)}") - -if not (positive_fraction > 0.95): - all_failures.append( - f"positive xs fraction {positive_fraction:.4f} <= 0.95" - ) - -if not math.isfinite(mean_log_xs): - all_failures.append(f"mean_log_xs is not finite: {mean_log_xs}") - -# --------------------------------------------------------------------------- -# Report -# --------------------------------------------------------------------------- -if total_retries > 0: - print(f"Resampled (NaN guard retry): {total_retries} resamples across {N_EVENTS} events") - -if rejected: - print(f"Rejected (NaN guard, retry budget exhausted): {len(rejected)}/{N_EVENTS} events ({100.0*len(rejected)/N_EVENTS:.2f}%)") - -if all_failures: - print(f"\nFAIL: " + "; ".join(all_failures)) - if failures: - print("First kinematic failures:") - for msg in failures[:10]: - print(f" {msg}") - sys.exit(1) -else: - retry_info = f" with {total_retries} resamples" if total_retries > 0 else "" - rej_info = f", {len(rejected)} unrecoverable" if rejected else "" - print(f"OK {len(sampled_kinematics)}/{N_EVENTS} sampled{retry_info}{rej_info}") - sys.exit(0) From b8e761a83f320e2b11c8cc475a8e282979f93a44 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 10:59:07 -0500 Subject: [PATCH 72/93] Charm-DIS cleanups: RNG reproducibility note, build tidy, example and controller fixes Document the mt19937_64 seeded-sequence change, fix the stale cstdint comment, drop stray blank lines in photospline.cmake, and make the DMesonELoss pybinding translation unit self-contained. In the example, register secondary interactions for all emitted D types and read the spline directory from SIREN_CHARM_SPLINE_DIR. --- cmake/Packages/photospline.cmake | 2 - .../private/pybindings/DMesonELoss.h | 3 + projects/utilities/private/Random.cxx | 2 +- .../utilities/public/SIREN/utilities/Random.h | 8 +- python/SIREN_Controller.py | 4 +- .../examples/example1/DIS_IceCube_charm.py | 73 ++++++++++++------- 6 files changed, 59 insertions(+), 33 deletions(-) diff --git a/cmake/Packages/photospline.cmake b/cmake/Packages/photospline.cmake index 1b72f0fd8..89059ffd6 100644 --- a/cmake/Packages/photospline.cmake +++ b/cmake/Packages/photospline.cmake @@ -20,8 +20,6 @@ if(NOT EXISTS "${PROJECT_SOURCE_DIR}/vendor/photospline/CMakeLists.txt") endif() #add_subdirectory(${PROJECT_SOURCE_DIR}/vendor/photospline EXCLUDE_FROM_ALL) - - # Override CMAKE_POLICY_VERSION_MINIMUM before adding subdirectory set(TEMP_CMAKE_POLICY_VERSION_MINIMUM ${CMAKE_POLICY_VERSION_MINIMUM}) set(CMAKE_POLICY_VERSION_MINIMUM 3.5) diff --git a/projects/interactions/private/pybindings/DMesonELoss.h b/projects/interactions/private/pybindings/DMesonELoss.h index dc3462393..56c554a64 100644 --- a/projects/interactions/private/pybindings/DMesonELoss.h +++ b/projects/interactions/private/pybindings/DMesonELoss.h @@ -7,6 +7,9 @@ #include #include "../../public/SIREN/interactions/CrossSection.h" +// Self-include the public class header so this TU does not rely on +// interactions.cxx pulling DMesonELoss.h in before this pybinding header. +#include "../../public/SIREN/interactions/DMesonELoss.h" #include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" #include "../../../dataclasses/public/SIREN/dataclasses/InteractionRecord.h" #include "../../../dataclasses/public/SIREN/dataclasses/InteractionSignature.h" diff --git a/projects/utilities/private/Random.cxx b/projects/utilities/private/Random.cxx index e79dc53ce..0d268a410 100644 --- a/projects/utilities/private/Random.cxx +++ b/projects/utilities/private/Random.cxx @@ -3,7 +3,7 @@ #include #include #include -#include // for uint32_t +#include // for uint64_t #include #include diff --git a/projects/utilities/public/SIREN/utilities/Random.h b/projects/utilities/public/SIREN/utilities/Random.h index 0e9a092c0..149d17607 100644 --- a/projects/utilities/public/SIREN/utilities/Random.h +++ b/projects/utilities/public/SIREN/utilities/Random.h @@ -8,7 +8,7 @@ // this implements a class to sample numbers just like in an i3 service #include // mt19937_64, uniform_real_distribution -#include // for uint32_t +#include // for uint64_t #include #include @@ -66,6 +66,12 @@ class SIREN_random{ // frequent internal state collisions across seeds, producing // identical event sequences. Switched to Mersenne Twister // (period 2^19937-1) to eliminate cross-seed duplicates. + // REPRODUCIBILITY NOTE: because the engine changed, a given fixed + // seed now produces a DIFFERENT deterministic sequence than the old + // std::default_random_engine. Serialization is unchanged (only the + // seed is archived, version 0), so existing serialized objects still + // load; but any cached samples or golden baselines keyed to a seed + // must be regenerated. Do not revert the engine to restore old output. std::mt19937_64 configuration; std::uniform_real_distribution generator; }; diff --git a/python/SIREN_Controller.py b/python/SIREN_Controller.py index 6086d6098..1aacf8e2c 100644 --- a/python/SIREN_Controller.py +++ b/python/SIREN_Controller.py @@ -690,9 +690,9 @@ def SaveEvents(self, filename, fill_tables_at_exit=True, datasets["num_interactions"].append(id+1) # save injector and weighter - self.injector.SaveInjector(filename) + # self.injector.SaveInjector(filename) # weighter saving not yet supported - self.weighter.SaveWeighter(filename) + #self.weighter.SaveWeighter(filename) # save events ak_array = ak.Array(datasets) diff --git a/resources/examples/example1/DIS_IceCube_charm.py b/resources/examples/example1/DIS_IceCube_charm.py index 0b29895fd..df57aef3f 100644 --- a/resources/examples/example1/DIS_IceCube_charm.py +++ b/resources/examples/example1/DIS_IceCube_charm.py @@ -5,8 +5,11 @@ Demonstrates the full modern charm chain: primary NuE CC+NC DIS (QuarkDISFromSpline, charm-target splines) - emits {charged lepton, Hadrons (shower), D meson (D0/D+/D0Bar/D+Bar)} - directly — no intermediate "Charm" quark, no separate hadronization step + emits {charged lepton, Hadrons (shower), D meson} directly -- no + intermediate "Charm" quark, no separate hadronization step. + For a neutrino primary QuarkDISFromSpline::DTypesForPrimary emits + {D0, D+, Ds+} (the charge conjugates {D0bar, D-, Ds-} are emitted for + an antineutrino primary). secondary on each D meson DMesonELoss (propagation energy loss) CharmMesonDecay (decay into leptons + K/pi) @@ -14,11 +17,12 @@ 10,000 events on IceCube, seed=1, volume injection inside the icecube sector, astrophysical power-law weighting. -Spline paths below are cluster-specific; edit `SPLINES_DIR` to point at your -own set of QuarkDIS charm-target spline files. +Splines are read from the SIREN_CHARM_SPLINE_DIR environment variable; point it +at your own set of QuarkDIS charm-target spline files before running. Usage: - python3 charm_example_new_idiom.py + export SIREN_CHARM_SPLINE_DIR=/path/to/M_Muon_New + python3 DIS_IceCube_charm.py """ import os @@ -32,10 +36,18 @@ # Config (edit for your setup) # ---------------------------------------------------------------------------- -SPLINES_DIR = ( - "/n/holylfs05/LABS/arguelles_delgado_lab/Everyone/miaochenjin/" - "DBSearch/Simulation/Resources/Splines/M_Muon_New" -) +# Spline directory: read from the SIREN_CHARM_SPLINE_DIR environment variable. +# These QuarkDIS charm-target .fits splines are large and machine-specific, so +# they are not bundled with SIREN. Point the variable at your own spline set, +# e.g. export SIREN_CHARM_SPLINE_DIR=/path/to/M_Muon_New +SPLINES_DIR = os.environ.get("SIREN_CHARM_SPLINE_DIR") +if not SPLINES_DIR: + raise RuntimeError( + "SIREN_CHARM_SPLINE_DIR is not set. Set it to the directory containing " + "the QuarkDIS charm-target spline files " + "(dsdxdy_nu-N-{cc,nc}-charm-*.fits and sigma_nu-N-{cc,nc}-charm-*.fits) " + "before running this example." + ) EXPERIMENT = "IceCube" PRIMARY_TYPE = siren.dataclasses.ParticleType.NuE NUMBER_OF_EVENTS = 10_000 @@ -131,37 +143,44 @@ def make_quark_dis_xs(pdf, target, current_type): # ---------------------------------------------------------------------------- -# Secondary interactions (D+ / D0) +# Secondary interactions (one entry per emitted D species) # # QuarkDISFromSpline emits a D meson directly as one of the primary-DIS # secondaries (not a bare charm quark), so no CharmHadronization step is # needed. We attach energy-loss and decay to each D species. +# +# IMPORTANT: every D type that QuarkDISFromSpline emits MUST have an entry here. +# The injector silently DROPS any emitted secondary that has no registered +# process (Injector.cxx: `if(it == secondary_process_map.end()) continue;`), so +# an unregistered species would be generated by the primary DIS and then never +# decay or lose energy -- a dangling secondary that distorts the event sample. +# +# For the NuE (neutrino) primary used here, DTypesForPrimary emits {D0, D+, Ds+}. +# (An antineutrino primary would instead emit {D0bar, D-, Ds-}; this example +# would then need D_TYPES updated to the conjugates.) We build the secondary +# dicts from D_TYPES so the registered set cannot silently desync from the +# emitter. DMesonELoss() handles all D species; the 2-body CharmMesonDecay +# supports D0/D+/Ds+ (and their conjugates). # ---------------------------------------------------------------------------- +# Must match QuarkDISFromSpline::DTypesForPrimary for a neutrino primary. +D_TYPES = [PT.D0, PT.DPlus, PT.DsPlus] + secondary_interactions = { - PT.DPlus: [ + d: [ siren.interactions.DMesonELoss(), - siren.interactions.CharmMesonDecay(primary_type=PT.DPlus), - ], - PT.D0: [ - siren.interactions.DMesonELoss(), - siren.interactions.CharmMesonDecay(primary_type=PT.D0), - ], + siren.interactions.CharmMesonDecay(primary_type=d), + ] + for d in D_TYPES } sec_vertex_dist = [siren.distributions.SecondaryPhysicalVertexDistribution()] -secondary_injection_distributions = { - PT.DPlus: sec_vertex_dist, - PT.D0: sec_vertex_dist, -} -secondary_physical_distributions = { - PT.DPlus: [], - PT.D0: [], -} +secondary_injection_distributions = {d: sec_vertex_dist for d in D_TYPES} +secondary_physical_distributions = {d: [] for d in D_TYPES} # ---------------------------------------------------------------------------- -# Injector — new idiom, property-setter API +# Injector -- new idiom, property-setter API # ---------------------------------------------------------------------------- injector = siren.injection.Injector() @@ -180,7 +199,7 @@ def make_quark_dis_xs(pdf, target, current_type): # ---------------------------------------------------------------------------- -# Weighter — new idiom, built after the injector is materialised +# Weighter -- new idiom, built after the injector is materialised # ---------------------------------------------------------------------------- weighter = siren.injection.Weighter() From 5c0cc3703b37166dd261beb38c1c5e28fa746faa Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 13:00:52 -0500 Subject: [PATCH 73/93] CharmMesonDecay/3Body: analytic V-A angle-average in weighting, numeric quadrature as regression oracle After the boost to the D rest frame the V-A weight is an exact quadratic in cos(theta_lepton), so the angle-average of the clipped accepted weight has a closed form (two positive-part quadratic integrals). FinalStateProbability uses it instead of Simpson quadrature; the numeric quadrature remains as a regression oracle the closed form must match. --- .../interactions/private/CharmMesonDecay.cxx | 119 ++++++++++++------ .../private/CharmMesonDecay3Body.cxx | 108 ++++++++++------ .../test/CharmMesonDecay3Body_TEST.cxx | 63 ++++++++++ .../private/test/CharmMesonDecay_TEST.cxx | 70 +++++++++++ .../SIREN/interactions/CharmMesonDecay.h | 5 +- .../SIREN/interactions/CharmMesonDecay3Body.h | 5 +- 6 files changed, 287 insertions(+), 83 deletions(-) diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx index e6fbdb8a6..b01268b57 100644 --- a/projects/interactions/private/CharmMesonDecay.cxx +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -712,48 +712,85 @@ double CharmMesonDecay::KStarMass() { return siren::utilities::Constants::KPrimePlusMass; } +namespace { +// Integral over [lo, hi] of max(0, a2*c^2 + a1*c + a0). Closed form with a +// linear/constant fallback for a2 ~ 0 and numerically stable quadratic roots. +// Used to evaluate the angle-averaged V-A weight analytically. +double integratePositivePartQuadratic(double a2, double a1, double a0, + double lo, double hi) { + if (hi <= lo) return 0.0; + auto F = [&](double c) { return a2 * c * c * c / 3.0 + a1 * c * c / 2.0 + a0 * c; }; + auto seg = [&](double x0, double x1) -> double { + if (x1 <= x0) return 0.0; + return F(x1) - F(x0); + }; + double scale = std::abs(a1) + std::abs(a0) + 1.0; + if (std::abs(a2) < 1e-12 * scale) { + // Linear q(c) = a1*c + a0 (or constant if a1 ~ 0). + if (std::abs(a1) < 1e-300) return (a0 > 0.0) ? seg(lo, hi) : 0.0; + double root = -a0 / a1; + if (a1 > 0.0) return seg(std::max(lo, root), hi); // q > 0 for c > root + return seg(lo, std::min(hi, root)); // q > 0 for c < root + } + double disc = a1 * a1 - 4.0 * a2 * a0; + if (disc <= 0.0) { + // No real roots: q keeps the sign of a2 everywhere. + return (a2 > 0.0) ? seg(lo, hi) : 0.0; + } + double sq = std::sqrt(disc); + double qq = -0.5 * (a1 + ((a1 >= 0.0) ? sq : -sq)); // stable roots + double r1 = qq / a2; + double r2 = a0 / qq; + double rlo = std::min(r1, r2), rhi = std::max(r1, r2); + if (a2 > 0.0) { + // q > 0 outside [rlo, rhi]. + return seg(lo, std::min(hi, rlo)) + seg(std::max(lo, rhi), hi); + } + // q > 0 inside (rlo, rhi). + return seg(std::max(lo, rlo), std::min(hi, rhi)); +} +} // namespace + double CharmMesonDecay::VAWeightAngleAverage(double mD, double mK, double ml, double m23) const { - // Numerically exact angle-average of the sampler's ACCEPTED V-A weight, - // max(0, min(wtME, wtMEmax)), over the lepton polar angle cosTheta in [-1,1]. - // wtME = mD * E_l * (p_nu . p_K) in the D rest frame, evaluated with the SAME - // rk::P4 boost operations SampleFinalState uses (kaon along +z, m23 system - // recoiling along -z), so the density reproduced here matches the sampler's - // accepted distribution exactly (weighting closure). wtMEmax mirrors the - // sampler's rejection ceiling so the (rare) saturated region is reproduced too. - // A deterministic Simpson rule replaces the previous closed form, which had a - // sign-region error in its quadratic-root branch. - double mnu = 0.0; - double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) - * (mD + mK - m23) * (mD - mK + m23)) / mD; - double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) - * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; - if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; - // Same matrix-element ceiling as SampleFinalState (meMode == 22). - double wtMEmax = std::min(std::pow(mD, 4) / 16.0, - mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); - // m23 system recoils along -z in the D rest frame; kaon sits along +z. - rk::P4 p4m23_Drest(geom3::Vector3(0.0, 0.0, -p1Abs), m23); - rk::Boost boost_m23_to_Drest = p4m23_Drest.labBoost(); - rk::P4 p4K_Drest(geom3::Vector3(0.0, 0.0, p1Abs), mK); - const int N = 400; // even -> composite Simpson - double h = 2.0 / N; - double sum = 0.0; - for (int i = 0; i <= N; ++i) { - double c = -1.0 + i * h; - double s = std::sqrt(std::max(0.0, 1.0 - c * c)); - geom3::Vector3 dir(s, 0.0, c); - rk::P4 p4l_m23rest(p23Abs * dir, ml); - rk::P4 p4nu_m23rest(-p23Abs * dir, mnu); - rk::P4 p4l_Drest = p4l_m23rest.boost(boost_m23_to_Drest); - rk::P4 p4nu_Drest = p4nu_m23rest.boost(boost_m23_to_Drest); - double w = mD * p4l_Drest.e() * p4nu_Drest.dot(p4K_Drest); - if (w < 0.0) w = 0.0; - if (w > wtMEmax) w = wtMEmax; - double wgt = (i == 0 || i == N) ? 1.0 : ((i % 2) ? 4.0 : 2.0); - sum += wgt * w; - } - double integral = sum * h / 3.0; // integral over c in [-1,1] - return 0.5 * integral; // average (c-measure has width 2) + // Analytic angle-average of the sampler's ACCEPTED V-A weight, + // (1/2) * integral_{-1}^{1} clamp(wtME(c), 0, wtMEmax) dc, + // where wtME(c) = mD * E_l(c) * (p_nu(c) . p_K) and c = cos(theta_lepton) in + // the (l,nu) rest frame. After the boost to the D rest frame both E_l and + // (p_nu . p_K) are linear in c, so wtME = pref * (a0 + a1 c + a2 c^2) is an + // exact quadratic. clamp(q, 0, M) = max(0, q) - max(0, q - M) turns the + // accept-reject ceiling into two positive-part integrals, each closed form. + // This reproduces SampleFinalState's accepted density exactly (weighting + // closure) with no quadrature error; the equivalent numeric quadrature is + // kept as the regression oracle VAWeightAngleAverageMatchesNumericReference. + double mnu = 0.0; + double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) + * (mD + mK - m23) * (mD - mK + m23)) / mD; + double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) + * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; + if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; + double E23 = std::sqrt(p1Abs * p1Abs + m23 * m23); + double bz = -p1Abs / E23; // (l,nu)-system velocity along -kaon axis + double gamma = E23 / m23; + double Elrest = std::sqrt(p23Abs * p23Abs + ml * ml); + double EK = std::sqrt(p1Abs * p1Abs + mK * mK); + // E_l = gamma (C + D c); (p_nu . p_K) = gamma p23Abs (A + B c). + double C = Elrest; + double D = bz * p23Abs; + double A = EK - p1Abs * bz; + double B = p1Abs - EK * bz; + double pref = mD * gamma * gamma * p23Abs; // > 0 + if (pref <= 0.0) return 0.0; + // wtME = pref * q(c), q(c) = (C + D c)(A + B c) = a0 + a1 c + a2 c^2. + double a0 = C * A; + double a1 = C * B + D * A; + double a2 = D * B; + // Same matrix-element ceiling as SampleFinalState (meMode == 22). + double wtMEmax = std::min(std::pow(mD, 4) / 16.0, + mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); + double qmax = wtMEmax / pref; + double integral = integratePositivePartQuadratic(a2, a1, a0, -1.0, 1.0) + - integratePositivePartQuadratic(a2, a1, a0 - qmax, -1.0, 1.0); + return 0.5 * pref * integral; // average over c (measure width 2) } double CharmMesonDecay::SampledQ2Density(double mD, double mK, double ml, double q2, bool apply_va) const { diff --git a/projects/interactions/private/CharmMesonDecay3Body.cxx b/projects/interactions/private/CharmMesonDecay3Body.cxx index b5e0b53ae..93ccd2b04 100644 --- a/projects/interactions/private/CharmMesonDecay3Body.cxx +++ b/projects/interactions/private/CharmMesonDecay3Body.cxx @@ -533,47 +533,75 @@ double CharmMesonDecay3Body::KStarMass() { return siren::utilities::Constants::KPrimePlusMass; } +namespace { +// Integral over [lo, hi] of max(0, a2*c^2 + a1*c + a0). Closed form with a +// linear/constant fallback for a2 ~ 0 and numerically stable quadratic roots. +double integratePositivePartQuadratic(double a2, double a1, double a0, + double lo, double hi) { + if (hi <= lo) return 0.0; + auto F = [&](double c) { return a2 * c * c * c / 3.0 + a1 * c * c / 2.0 + a0 * c; }; + auto seg = [&](double x0, double x1) -> double { + if (x1 <= x0) return 0.0; + return F(x1) - F(x0); + }; + double scale = std::abs(a1) + std::abs(a0) + 1.0; + if (std::abs(a2) < 1e-12 * scale) { + if (std::abs(a1) < 1e-300) return (a0 > 0.0) ? seg(lo, hi) : 0.0; + double root = -a0 / a1; + if (a1 > 0.0) return seg(std::max(lo, root), hi); + return seg(lo, std::min(hi, root)); + } + double disc = a1 * a1 - 4.0 * a2 * a0; + if (disc <= 0.0) { + return (a2 > 0.0) ? seg(lo, hi) : 0.0; + } + double sq = std::sqrt(disc); + double qq = -0.5 * (a1 + ((a1 >= 0.0) ? sq : -sq)); + double r1 = qq / a2; + double r2 = a0 / qq; + double rlo = std::min(r1, r2), rhi = std::max(r1, r2); + if (a2 > 0.0) { + return seg(lo, std::min(hi, rlo)) + seg(std::max(lo, rhi), hi); + } + return seg(std::max(lo, rlo), std::min(hi, rhi)); +} +} // namespace + double CharmMesonDecay3Body::VAWeightAngleAverage(double mD, double mK, double ml, double m23) const { - // Numerically exact angle-average of the sampler's ACCEPTED V-A weight, - // max(0, min(wtME, wtMEmax)), over the lepton polar angle cosTheta in [-1,1]. - // wtME = mD * E_l * (p_nu . p_K) in the D rest frame, evaluated with the SAME - // rk::P4 boost operations SampleFinalState uses (kaon along +z, m23 system - // recoiling along -z), so the reproduced density matches the sampler exactly - // (closure). wtMEmax mirrors the sampler's rejection ceiling. A deterministic - // Simpson rule replaces the previous closed form, which had a sign-region - // error in its quadratic-root branch. - double mnu = 0.0; - double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) - * (mD + mK - m23) * (mD - mK + m23)) / mD; - double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) - * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; - if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; - // Same matrix-element ceiling as SampleFinalState (meMode == 22). - double wtMEmax = std::min(std::pow(mD, 4) / 16.0, - mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); - // m23 system recoils along -z in the D rest frame; kaon sits along +z. - rk::P4 p4m23_Drest(geom3::Vector3(0.0, 0.0, -p1Abs), m23); - rk::Boost boost_m23_to_Drest = p4m23_Drest.labBoost(); - rk::P4 p4K_Drest(geom3::Vector3(0.0, 0.0, p1Abs), mK); - const int N = 400; // even -> composite Simpson - double h = 2.0 / N; - double sum = 0.0; - for (int i = 0; i <= N; ++i) { - double c = -1.0 + i * h; - double s = std::sqrt(std::max(0.0, 1.0 - c * c)); - geom3::Vector3 dir(s, 0.0, c); - rk::P4 p4l_m23rest(p23Abs * dir, ml); - rk::P4 p4nu_m23rest(-p23Abs * dir, mnu); - rk::P4 p4l_Drest = p4l_m23rest.boost(boost_m23_to_Drest); - rk::P4 p4nu_Drest = p4nu_m23rest.boost(boost_m23_to_Drest); - double w = mD * p4l_Drest.e() * p4nu_Drest.dot(p4K_Drest); - if (w < 0.0) w = 0.0; - if (w > wtMEmax) w = wtMEmax; - double wgt = (i == 0 || i == N) ? 1.0 : ((i % 2) ? 4.0 : 2.0); - sum += wgt * w; - } - double integral = sum * h / 3.0; // integral over c in [-1,1] - return 0.5 * integral; // average (c-measure has width 2) + // Analytic angle-average of the sampler's ACCEPTED V-A weight, + // (1/2) * integral_{-1}^{1} clamp(wtME(c), 0, wtMEmax) dc. + // wtME(c) = mD * E_l(c) * (p_nu(c) . p_K) is an exact quadratic in + // c = cos(theta_lepton) after the boost to the D rest frame, so the positive + // and clipped parts integrate in closed form via + // clamp(q, 0, M) = max(0, q) - max(0, q - M). Matches SampleFinalState's + // accepted density exactly (closure) with no quadrature error; the numeric + // quadrature lives in VAWeightAngleAverageMatchesNumericReference. + double mnu = 0.0; + double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) + * (mD + mK - m23) * (mD - mK + m23)) / mD; + double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) + * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; + if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; + double E23 = std::sqrt(p1Abs * p1Abs + m23 * m23); + double bz = -p1Abs / E23; + double gamma = E23 / m23; + double Elrest = std::sqrt(p23Abs * p23Abs + ml * ml); + double EK = std::sqrt(p1Abs * p1Abs + mK * mK); + double C = Elrest; + double D = bz * p23Abs; + double A = EK - p1Abs * bz; + double B = p1Abs - EK * bz; + double pref = mD * gamma * gamma * p23Abs; + if (pref <= 0.0) return 0.0; + double a0 = C * A; + double a1 = C * B + D * A; + double a2 = D * B; + double wtMEmax = std::min(std::pow(mD, 4) / 16.0, + mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); + double qmax = wtMEmax / pref; + double integral = integratePositivePartQuadratic(a2, a1, a0, -1.0, 1.0) + - integratePositivePartQuadratic(a2, a1, a0 - qmax, -1.0, 1.0); + return 0.5 * pref * integral; } double CharmMesonDecay3Body::SampledQ2Density(double mD, double mK, double ml, double q2, bool apply_va) const { diff --git a/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx b/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx index d9991eac0..3353c9414 100644 --- a/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx +++ b/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx @@ -343,6 +343,69 @@ TEST(CharmMesonDecay3Body, FinalStateProbabilityClosure) { } } +// --- Analytic angle-average matches a numeric quadrature oracle ------------ +// +// The weighting code uses the CLOSED-FORM CharmMesonDecay3Body:: +// VAWeightAngleAverage. This is the "shunted integral": a high-resolution +// numeric quadrature of the identical clamped V-A weight (same rk::P4 boosts as +// SampleFinalState) that the analytic form must reproduce, guarding against any +// algebra error in the closed form. +namespace { +double numericVAWeightAngleAverage3B(double mD, double mK, double ml, double m23) { + double mnu = 0.0; + double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) + * (mD + mK - m23) * (mD - mK + m23)) / mD; + double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) + * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; + if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; + double wtMEmax = std::min(std::pow(mD, 4) / 16.0, + mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); + rk::P4 p4m23(geom3::Vector3(0.0, 0.0, -p1Abs), m23); + rk::Boost boost = p4m23.labBoost(); + rk::P4 p4K(geom3::Vector3(0.0, 0.0, p1Abs), mK); + const int N = 20000; + double h = 2.0 / N, sum = 0.0; + for (int i = 0; i <= N; ++i) { + double c = -1.0 + i * h; + double s = std::sqrt(std::max(0.0, 1.0 - c * c)); + geom3::Vector3 dir(s, 0.0, c); + rk::P4 p4l = rk::P4(p23Abs * dir, ml).boost(boost); + rk::P4 p4nu = rk::P4(-p23Abs * dir, mnu).boost(boost); + double w = mD * p4l.e() * p4nu.dot(p4K); + if (w < 0.0) w = 0.0; + if (w > wtMEmax) w = wtMEmax; + double wgt = (i == 0 || i == N) ? 1.0 : ((i % 2) ? 4.0 : 2.0); + sum += wgt * w; + } + return 0.5 * (sum * h / 3.0); +} +} // namespace + +TEST(CharmMesonDecay3Body, VAWeightAngleAverageMatchesNumericReference) { + CharmMesonDecay3Body d0(ParticleType::D0), dp(ParticleType::DPlus); + struct Case { CharmMesonDecay3Body* dec; double mD; double mK; double ml; }; + std::vector cases = { + {&d0, Constants::D0Mass, Constants::KMinusMass, Constants::electronMass}, + {&d0, Constants::D0Mass, Constants::KMinusMass, Constants::muonMass}, + {&d0, Constants::D0Mass, Constants::KPrimePlusMass, Constants::electronMass}, + {&dp, Constants::DPlusMass, Constants::K0Mass, Constants::electronMass}, + {&dp, Constants::DPlusMass, Constants::KPrimePlusMass, Constants::muonMass}, + }; + for (auto & cs : cases) { + double m23Min = cs.ml; + double m23Max = cs.mD - cs.mK; + const int NG = 40; + for (int g = 1; g < NG; ++g) { + double m23 = m23Min + (m23Max - m23Min) * (double)g / NG; + double ana = cs.dec->VAWeightAngleAverage(cs.mD, cs.mK, cs.ml, m23); + double num = numericVAWeightAngleAverage3B(cs.mD, cs.mK, cs.ml, m23); + double tol = 1e-4 * std::abs(num) + 1e-12; + EXPECT_NEAR(ana, num, tol) + << "mD=" << cs.mD << " mK=" << cs.mK << " ml=" << cs.ml << " m23=" << m23; + } + } +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx index 76676cf46..c198db8e9 100644 --- a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx +++ b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx @@ -28,6 +28,9 @@ #include +#include +#include + #include "SIREN/utilities/Interpolator.h" #include "SIREN/utilities/Integration.h" #include "SIREN/utilities/Constants.h" @@ -432,3 +435,70 @@ TEST(CharmMesonDecay, UnsupportedSignaturesThrow) { bad_secondaries.signature.secondary_types = {ParticleType::PiPlus, ParticleType::PiMinus}; EXPECT_THROW(decay.TotalDecayWidthForFinalState(bad_secondaries), std::runtime_error); } + +// --- Test 5: analytic angle-average matches a numeric quadrature oracle ----- +// +// The weighting code (FinalStateProbability via SampledQ2Density) uses the +// CLOSED-FORM CharmMesonDecay::VAWeightAngleAverage. This test is the "shunted +// integral": a high-resolution numeric quadrature of the identical clamped V-A +// weight, evaluated with the SAME rk::P4 boosts SampleFinalState uses, that the +// analytic form must reproduce. It guards against any future algebra error in +// the closed form (the kind that produced the original closure break). +namespace { +double numericVAWeightAngleAverage(double mD, double mK, double ml, double m23) { + double mnu = 0.0; + double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) + * (mD + mK - m23) * (mD - mK + m23)) / mD; + double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) + * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; + if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; + double wtMEmax = std::min(std::pow(mD, 4) / 16.0, + mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); + rk::P4 p4m23(geom3::Vector3(0.0, 0.0, -p1Abs), m23); + rk::Boost boost = p4m23.labBoost(); + rk::P4 p4K(geom3::Vector3(0.0, 0.0, p1Abs), mK); + const int N = 20000; // even -> composite Simpson + double h = 2.0 / N, sum = 0.0; + for (int i = 0; i <= N; ++i) { + double c = -1.0 + i * h; + double s = std::sqrt(std::max(0.0, 1.0 - c * c)); + geom3::Vector3 dir(s, 0.0, c); + rk::P4 p4l = rk::P4(p23Abs * dir, ml).boost(boost); + rk::P4 p4nu = rk::P4(-p23Abs * dir, mnu).boost(boost); + double w = mD * p4l.e() * p4nu.dot(p4K); + if (w < 0.0) w = 0.0; + if (w > wtMEmax) w = wtMEmax; + double wgt = (i == 0 || i == N) ? 1.0 : ((i % 2) ? 4.0 : 2.0); + sum += wgt * w; + } + return 0.5 * (sum * h / 3.0); +} +} // namespace + +TEST(CharmMesonDecay, VAWeightAngleAverageMatchesNumericReference) { + CharmMesonDecay d0(ParticleType::D0), dp(ParticleType::DPlus); + struct Case { CharmMesonDecay* dec; double mD; double mK; double ml; }; + std::vector cases = { + {&d0, Constants::D0Mass, Constants::KMinusMass, Constants::electronMass}, + {&d0, Constants::D0Mass, Constants::KMinusMass, Constants::muonMass}, + {&d0, Constants::D0Mass, Constants::KPrimePlusMass, Constants::electronMass}, + {&dp, Constants::DPlusMass, Constants::K0Mass, Constants::electronMass}, + {&dp, Constants::DPlusMass, Constants::K0Mass, Constants::muonMass}, + {&dp, Constants::DPlusMass, Constants::KPrimePlusMass, Constants::muonMass}, + }; + for (auto & cs : cases) { + double m23Min = cs.ml; + double m23Max = cs.mD - cs.mK; + const int NG = 40; + for (int g = 1; g < NG; ++g) { + double m23 = m23Min + (m23Max - m23Min) * (double)g / NG; + double ana = cs.dec->VAWeightAngleAverage(cs.mD, cs.mK, cs.ml, m23); + double num = numericVAWeightAngleAverage(cs.mD, cs.mK, cs.ml, m23); + // Tolerance is quadrature-limited (Simpson degrades to O(h^2) at the + // clamp kinks); 1e-4 relative is far tighter than any real algebra bug. + double tol = 1e-4 * std::abs(num) + 1e-12; + EXPECT_NEAR(ana, num, tol) + << "mD=" << cs.mD << " mK=" << cs.mK << " ml=" << cs.ml << " m23=" << m23; + } + } +} diff --git a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h index 3eb21f386..f7a601989 100644 --- a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h +++ b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h @@ -39,7 +39,6 @@ friend cereal::access; // Shared closure helpers: SampleFinalState's density and FinalStateProbability // both build on these, so Sample == Density by construction. static double KStarMass(); - double VAWeightAngleAverage(double mD, double mK, double ml, double m23) const; double SampledQ2Density(double mD, double mK, double ml, double q2, bool apply_va) const; double SampledQ2Normalization(double mD, double mK, double ml, bool apply_va) const; // Per-component normalization cache (not serialized; keyed by mass set). @@ -49,6 +48,10 @@ friend cereal::access; CharmMesonDecay(siren::dataclasses::Particle::ParticleType primary); virtual bool equal(Decay const & other) const override; static double particleMass(siren::dataclasses::ParticleType particle); + // Analytic angle-average of the accepted V-A weight (q^2 density factor). + // Public so the closure/regression test can check it against a numeric + // quadrature oracle; it is a pure function of the decay masses and m23. + double VAWeightAngleAverage(double mD, double mK, double ml, double m23) const; double TotalDecayWidth(dataclasses::InteractionRecord const &) const override; double TotalDecayWidth(siren::dataclasses::Particle::ParticleType primary) const override; double TotalDecayWidthForFinalState(dataclasses::InteractionRecord const &) const override; diff --git a/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h b/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h index 54711c80c..f1a344619 100644 --- a/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h +++ b/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h @@ -50,7 +50,6 @@ friend cereal::access; // Shared closure helpers: SampleFinalState's density and FinalStateProbability // both build on these, so Sample == Density by construction. static double KStarMass(); - double VAWeightAngleAverage(double mD, double mK, double ml, double m23) const; double SampledQ2Density(double mD, double mK, double ml, double q2, bool apply_va) const; double SampledQ2Normalization(double mD, double mK, double ml, bool apply_va) const; // Per-component normalization cache (not serialized; keyed by mass set). @@ -60,6 +59,10 @@ friend cereal::access; CharmMesonDecay3Body(siren::dataclasses::Particle::ParticleType primary); virtual bool equal(Decay const & other) const override; static double particleMass(siren::dataclasses::ParticleType particle); + // Analytic angle-average of the accepted V-A weight (q^2 density factor). + // Public so the closure/regression test can check it against a numeric + // quadrature oracle; pure function of the decay masses and m23. + double VAWeightAngleAverage(double mD, double mK, double ml, double m23) const; double TotalDecayWidth(dataclasses::InteractionRecord const &) const override; double TotalDecayWidth(siren::dataclasses::Particle::ParticleType primary) const override; double TotalDecayWidthForFinalState(dataclasses::InteractionRecord const &) const override; From dea78738a9a7f44fafddebc25f001f245155eb08 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 13:52:51 -0500 Subject: [PATCH 74/93] Fix charm decay serialization and re-enable Weighter save/load CharmMesonDecay and CharmMesonDecay3Body used cereal save() + load_and_construct() while being default-constructible, so cereal skipped load_and_construct, never consumed the serialized body, and corrupted the rest of the archive. Replace it with a member load() paired with the default constructor. Implement Weighter::LoadWeighter (it was a stub) and make Weighter::Initialize idempotent. Add UnitTest_CharmSerialization, whose trailing sentinel fails if a body is skipped on load. --- projects/injection/CMakeLists.txt | 4 + projects/injection/private/Weighter.cxx | 16 +- .../private/test/CharmSerialization_TEST.cxx | 144 ++++++++++++++++++ .../SIREN/interactions/CharmMesonDecay.h | 15 +- .../SIREN/interactions/CharmMesonDecay3Body.h | 11 +- python/SIREN_Controller.py | 8 +- python/Weighter.py | 22 +++ 7 files changed, 204 insertions(+), 16 deletions(-) create mode 100644 projects/injection/private/test/CharmSerialization_TEST.cxx diff --git a/projects/injection/CMakeLists.txt b/projects/injection/CMakeLists.txt index dc2cf597b..289be1c16 100644 --- a/projects/injection/CMakeLists.txt +++ b/projects/injection/CMakeLists.txt @@ -40,6 +40,10 @@ install(DIRECTORY "${PROJECT_SOURCE_DIR}/projects/injection/public/" #package_add_test(UnitTest_Injector ${PROJECT_SOURCE_DIR}/projects/injection/private/test/Injector_TEST.cxx) package_add_test(UnitTest_Weighter ${PROJECT_SOURCE_DIR}/projects/injection/private/test/Weighter_TEST.cxx) +package_add_test(UnitTest_CharmSerialization ${PROJECT_SOURCE_DIR}/projects/injection/private/test/CharmSerialization_TEST.cxx) +# The injection headers pull in Pybind11Trampoline.h, so this TU needs the +# pybind11 headers (header-only; no Python embedding required). +target_link_libraries(UnitTest_CharmSerialization pybind11::headers) if(NOT ${CIBUILDWHEEL}) package_add_test(UnitTest_CCM_HNL ${PROJECT_SOURCE_DIR}/projects/injection/private/test/CCM_HNL_TEST.cxx) target_link_libraries(UnitTest_CCM_HNL pybind11::embed) diff --git a/projects/injection/private/Weighter.cxx b/projects/injection/private/Weighter.cxx index 4c9c389a9..221f1787a 100644 --- a/projects/injection/private/Weighter.cxx +++ b/projects/injection/private/Weighter.cxx @@ -49,6 +49,10 @@ using detector::DetectorDirection; //--------------- void Weighter::Initialize() { + // Idempotent: clear any previously-built weighters so Initialize() can be + // called again after LoadWeighter() or after injectors are overwritten. + primary_process_weighters.clear(); + secondary_process_weighter_maps.clear(); int i = 0; primary_process_weighters.reserve(injectors.size()); secondary_process_weighter_maps.reserve(injectors.size()); @@ -194,11 +198,17 @@ void Weighter::SaveWeighter(std::string const & filename) const { } void Weighter::LoadWeighter(std::string const & filename) { - std::cout << "Weighter loading not yet supported... sorry!\n"; - exit(0); std::ifstream is(filename+".siren_weighter", std::ios::binary); ::cereal::BinaryInputArchive archive(is); - //this->load(archive,0); + // Read the same members SaveWeighter (Weighter::save) wrote, in the same + // order; the polymorphic Injector / PhysicalProcess (and the cross sections + // and decays they hold) are reconstructed via their registered cereal + // load hooks. Then rebuild the per-process weighters. + archive(::cereal::make_nvp("Injectors", injectors)); + archive(::cereal::make_nvp("DetectorModel", detector_model)); + archive(::cereal::make_nvp("PrimaryPhysicalProcess", primary_physical_process)); + archive(::cereal::make_nvp("SecondaryPhysicalProcesses", secondary_physical_processes)); + Initialize(); } Weighter::Weighter(std::vector> injectors, std::shared_ptr detector_model, std::shared_ptr primary_physical_process, std::vector> secondary_physical_processes) diff --git a/projects/injection/private/test/CharmSerialization_TEST.cxx b/projects/injection/private/test/CharmSerialization_TEST.cxx new file mode 100644 index 000000000..68f60d461 --- /dev/null +++ b/projects/injection/private/test/CharmSerialization_TEST.cxx @@ -0,0 +1,144 @@ +// Serialization round-trip tests for the charm-DIS decay classes and the Weighter. +// +// These guard two previously-broken behaviors: +// 1. CharmMesonDecay / CharmMesonDecay3Body are default-constructible but used +// cereal save() + load_and_construct(). cereal does not invoke +// load_and_construct for a default-constructible type, so the serialized +// body was never read on load -- the object survived only because its +// primary_types set is a fixed const member, while every byte of the body +// was left in the stream, corrupting whatever followed. They now use a +// member load(); the trailing-sentinel assertions below catch any +// regression to a body-skipping load. +// 2. Weighter::LoadWeighter was a stub that printed "not yet supported" and +// called exit(0). It now deserializes the weighter; the round-trip reaches +// its assertions (it would have killed the test process before). + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "SIREN/interactions/Decay.h" +#include "SIREN/interactions/CharmMesonDecay.h" +#include "SIREN/interactions/CharmMesonDecay3Body.h" +#include "SIREN/interactions/InteractionCollection.h" +#include "SIREN/injection/Process.h" +#include "SIREN/injection/Injector.h" +#include "SIREN/injection/Weighter.h" +#include "SIREN/detector/DetectorModel.h" +#include "SIREN/dataclasses/Particle.h" + +using namespace siren::interactions; +using namespace siren::injection; +using siren::dataclasses::ParticleType; +using siren::detector::DetectorModel; + +namespace { +// Round-trip a polymorphic Decay through a binary archive followed by a sentinel +// int. The sentinel reads back correctly only if the decay body was fully +// consumed on load -- i.e. it directly detects the body-skip serialization bug. +std::shared_ptr roundtrip_decay_with_sentinel(std::shared_ptr orig, + bool & sentinel_ok) { + const int kSentinel = 0x5A5A5A; + std::stringstream ss; + { + cereal::BinaryOutputArchive oarchive(ss); + oarchive(orig); + int s = kSentinel; + oarchive(s); + } + std::shared_ptr loaded; + int s = 0; + { + cereal::BinaryInputArchive iarchive(ss); + iarchive(loaded); + iarchive(s); + } + sentinel_ok = (s == kSentinel); + return loaded; +} + +std::string read_file(std::string const & path) { + std::ifstream is(path, std::ios::binary); + std::stringstream buf; + buf << is.rdbuf(); + return buf.str(); +} +} // namespace + +// --- Polymorphic Decay round-trips (load path consumes the full body) -------- + +TEST(CharmSerialization, CharmMesonDecayPolymorphicRoundTrip) { + std::shared_ptr orig = std::make_shared(ParticleType::D0); + bool sentinel_ok = false; + std::shared_ptr loaded = roundtrip_decay_with_sentinel(orig, sentinel_ok); + ASSERT_NE(loaded, nullptr); + EXPECT_NE(std::dynamic_pointer_cast(loaded), nullptr); + EXPECT_TRUE(orig->equal(*loaded)); + EXPECT_TRUE(loaded->equal(*orig)); + EXPECT_EQ(orig->GetPossibleSignatures().size(), + loaded->GetPossibleSignatures().size()); + EXPECT_TRUE(sentinel_ok) << "decay body was not fully consumed on load"; +} + +TEST(CharmSerialization, CharmMesonDecay3BodyPolymorphicRoundTrip) { + std::shared_ptr orig = std::make_shared(ParticleType::D0); + bool sentinel_ok = false; + std::shared_ptr loaded = roundtrip_decay_with_sentinel(orig, sentinel_ok); + ASSERT_NE(loaded, nullptr); + EXPECT_NE(std::dynamic_pointer_cast(loaded), nullptr); + EXPECT_TRUE(orig->equal(*loaded)); + EXPECT_TRUE(loaded->equal(*orig)); + EXPECT_EQ(orig->GetPossibleSignatures().size(), + loaded->GetPossibleSignatures().size()); + EXPECT_TRUE(sentinel_ok) << "decay body was not fully consumed on load"; +} + +// --- Weighter SaveWeighter/LoadWeighter with a charm decay process ----------- + +TEST(CharmSerialization, WeighterWithCharmDecaySaveLoad) { + auto detector_model = std::make_shared(); + std::shared_ptr decay = std::make_shared(ParticleType::D0); + auto ic = std::make_shared( + ParticleType::D0, std::vector>{decay}); + auto phys = std::make_shared(ParticleType::D0, ic); + + // Empty injectors keep the Weighter construction data-free (Initialize()'s + // per-injector loop is then a no-op); the charm decay still rides through + // the serialized PhysicalProcess graph. + std::vector> no_injectors; + std::vector> no_secondaries; + Weighter w(no_injectors, detector_model, phys, no_secondaries); + + std::string base = "/tmp/siren_charm_weighter_test"; + std::string base2 = base + "_rt"; + std::remove((base + ".siren_weighter").c_str()); + std::remove((base2 + ".siren_weighter").c_str()); + + ASSERT_NO_THROW(w.SaveWeighter(base)); // writes .siren_weighter + + // The filename constructor calls LoadWeighter (previously a stub that + // exit(0)'d). Reaching the next statement at all proves loading is enabled. + std::unique_ptr w2; + ASSERT_NO_THROW(w2.reset(new Weighter(no_injectors, base))); + ASSERT_NE(w2, nullptr); + + // Re-serialize the loaded weighter; a faithful round-trip is byte-identical. + ASSERT_NO_THROW(w2->SaveWeighter(base2)); + std::string bytes1 = read_file(base + ".siren_weighter"); + std::string bytes2 = read_file(base2 + ".siren_weighter"); + EXPECT_FALSE(bytes1.empty()); + EXPECT_EQ(bytes1, bytes2); + + std::remove((base + ".siren_weighter").c_str()); + std::remove((base2 + ".siren_weighter").c_str()); +} diff --git a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h index f7a601989..bd62e3715 100644 --- a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h +++ b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h @@ -45,7 +45,7 @@ friend cereal::access; mutable std::map norm_cache; public: CharmMesonDecay(); - CharmMesonDecay(siren::dataclasses::Particle::ParticleType primary); + CharmMesonDecay(siren::dataclasses::Particle::ParticleType primary); virtual bool equal(Decay const & other) const override; static double particleMass(siren::dataclasses::ParticleType particle); // Analytic angle-average of the accepted V-A weight (q^2 density factor). @@ -77,13 +77,18 @@ friend cereal::access; } } template - void load_and_construct(Archive & archive, cereal::construct & construct, std::uint32_t version) { + void load(Archive & archive, std::uint32_t version) { if(version == 0) { + // CharmMesonDecay is default-constructible and primary_types is a + // fixed const member, so cereal default-constructs then calls this + // load(). The archived PrimaryTypes is read into a temporary to + // consume the stream symmetrically with save() (its value is + // invariant across instances). A load_and_construct here would be + // bypassed for a default-constructible type, leaving the body + // unread and corrupting any following data in the archive. std::set _primary_types; - archive(::cereal::make_nvp("PrimaryTypes", _primary_types)); - construct(_primary_types); - archive(::cereal::make_nvp("Decay", cereal::virtual_base_class(construct.ptr()))); + archive(::cereal::make_nvp("Decay", cereal::virtual_base_class(this))); } else { throw std::runtime_error("CharmMesonDecay only supports version <= 0!"); } diff --git a/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h b/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h index f1a344619..fd22bcbff 100644 --- a/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h +++ b/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h @@ -88,13 +88,16 @@ friend cereal::access; } } template - void load_and_construct(Archive & archive, cereal::construct & construct, std::uint32_t version) { + void load(Archive & archive, std::uint32_t version) { if(version == 0) { + // Default-constructible with a fixed const primary_types, so cereal + // default-constructs then calls this load(). Read PrimaryTypes into a + // temporary to consume the stream symmetrically with save(); a + // load_and_construct would be bypassed for a default-constructible + // type, leaving the body unread and corrupting following archive data. std::set _primary_types; - archive(::cereal::make_nvp("PrimaryTypes", _primary_types)); - construct(_primary_types); - archive(::cereal::make_nvp("Decay", cereal::virtual_base_class(construct.ptr()))); + archive(::cereal::make_nvp("Decay", cereal::virtual_base_class(this))); } else { throw std::runtime_error("CharmMesonDecay3Body only supports version <= 0!"); } diff --git a/python/SIREN_Controller.py b/python/SIREN_Controller.py index 1aacf8e2c..844b630eb 100644 --- a/python/SIREN_Controller.py +++ b/python/SIREN_Controller.py @@ -689,10 +689,10 @@ def SaveEvents(self, filename, fill_tables_at_exit=True, datasets["num_secondaries"][-1].append(isec+1) datasets["num_interactions"].append(id+1) - # save injector and weighter - # self.injector.SaveInjector(filename) - # weighter saving not yet supported - #self.weighter.SaveWeighter(filename) + # save injector and weighter (writes .siren_injector and + # .siren_weighter alongside the event file) + self.injector.save(filename + ".siren_injector") + self.weighter.save(filename) # save events ak_array = ak.Array(datasets) diff --git a/python/Weighter.py b/python/Weighter.py index 69f8c4280..aee604d37 100644 --- a/python/Weighter.py +++ b/python/Weighter.py @@ -308,3 +308,25 @@ def event_weight(self, interaction_tree: InteractionTree) -> float: float: The calculated event weight. """ return self(interaction_tree) + + def save(self, filename: str): + """ + Serialize the weighter to ``.siren_weighter``. + + Args: + filename: Base path; the ".siren_weighter" suffix is added. + """ + if self.__weighter is None: + self.__initialize_weighter() + self.__weighter.SaveWeighter(filename) + + def load(self, filename: str): + """ + Restore the weighter state from ``.siren_weighter``. + + Args: + filename: Base path; the ".siren_weighter" suffix is added. + """ + if self.__weighter is None: + self.__initialize_weighter() + self.__weighter.LoadWeighter(filename) From 87f770347aa83914d7ce0caf3a3b16d14ce68550 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 14:07:34 -0500 Subject: [PATCH 75/93] Make load_and_construct static in HNL/EW/dipole/elastic interaction classes cereal ignores a non-static member load_and_construct, so HNLDecay, ElectroweakDecay, HNLDipoleDecay, ElasticScattering, and HNLDipoleFromTable were default-constructed without reading their bodies, corrupting the archive. Add the static keyword. Add UnitTest_InteractionSerialization round-trips with a trailing sentinel. --- projects/interactions/CMakeLists.txt | 5 + .../test/InteractionSerialization_TEST.cxx | 115 ++++++++++++++++++ .../SIREN/interactions/ElasticScattering.h | 2 +- .../SIREN/interactions/ElectroweakDecay.h | 2 +- .../public/SIREN/interactions/HNLDecay.h | 2 +- .../SIREN/interactions/HNLDipoleDecay.h | 2 +- .../SIREN/interactions/HNLDipoleFromTable.h | 2 +- 7 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 projects/interactions/private/test/InteractionSerialization_TEST.cxx diff --git a/projects/interactions/CMakeLists.txt b/projects/interactions/CMakeLists.txt index 8e6d95724..8bde4c528 100644 --- a/projects/interactions/CMakeLists.txt +++ b/projects/interactions/CMakeLists.txt @@ -113,6 +113,11 @@ package_add_test(UnitTest_CharmMesonDecay3Body ${PROJECT_SOURCE_DIR}/projects/in # fragmentation pdf/cdf). package_add_test(UnitTest_QuarkDISDensityContract ${PROJECT_SOURCE_DIR}/projects/interactions/private/test/QuarkDISDensityContract_TEST.cxx) +# Serialization round-trips for the interaction classes whose load_and_construct +# was made static (HNLDecay, ElectroweakDecay, HNLDipoleDecay, ElasticScattering, +# HNLDipoleFromTable). +package_add_test(UnitTest_InteractionSerialization ${PROJECT_SOURCE_DIR}/projects/interactions/private/test/InteractionSerialization_TEST.cxx) + # Real PythiaDISCrossSection charm-DIS closure/normalization regression test. # Only built with Pythia8 support; needs charm-DIS spline files via env vars # (SIREN_PYTHIA_TEST_DSDXDY, SIREN_PYTHIA_TEST_SIGMA), otherwise it SKIPs. diff --git a/projects/interactions/private/test/InteractionSerialization_TEST.cxx b/projects/interactions/private/test/InteractionSerialization_TEST.cxx new file mode 100644 index 000000000..2da3134d7 --- /dev/null +++ b/projects/interactions/private/test/InteractionSerialization_TEST.cxx @@ -0,0 +1,115 @@ +// Serialization round-trip tests for the interaction classes whose +// load_and_construct was a NON-STATIC member. cereal only recognizes a STATIC +// load_and_construct (or a non-member specialization); a non-static one is +// silently ignored, so cereal default-constructed the object and never read the +// serialized body -- corrupting everything that followed it in the archive. +// These classes were changed to `static void load_and_construct`; the trailing +// sentinel below reads back correctly only if the body is fully consumed, so it +// directly guards against a regression to a body-skipping load. (DarkNewsDecay +// is intentionally excluded: it is abstract / python-backed and cannot be +// constructed by cereal here.) + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "SIREN/interactions/Decay.h" +#include "SIREN/interactions/CrossSection.h" +#include "SIREN/interactions/HNLDecay.h" +#include "SIREN/interactions/ElectroweakDecay.h" +#include "SIREN/interactions/HNLDipoleDecay.h" +#include "SIREN/interactions/ElasticScattering.h" +#include "SIREN/interactions/HNLDipoleFromTable.h" +#include "SIREN/dataclasses/Particle.h" + +using namespace siren::interactions; +using siren::dataclasses::ParticleType; + +namespace { +// Round-trip a polymorphic base pointer through a binary archive, followed by a +// sentinel int. The sentinel reads back correctly only if the object's body was +// fully consumed on load. +template +std::shared_ptr roundtrip_with_sentinel(std::shared_ptr orig, bool & sentinel_ok) { + const int kSentinel = 0x5A5A5A; + std::stringstream ss; + { + cereal::BinaryOutputArchive oarchive(ss); + oarchive(orig); + int s = kSentinel; + oarchive(s); + } + std::shared_ptr loaded; + int s = 0; + { + cereal::BinaryInputArchive iarchive(ss); + iarchive(loaded); + iarchive(s); + } + sentinel_ok = (s == kSentinel); + return loaded; +} +} // namespace + +TEST(InteractionSerialization, HNLDecayRoundTrip) { + std::shared_ptr orig = std::make_shared( + 0.1, std::vector{0.0, 0.0, 1e-3}, HNLDecay::ChiralNature::Majorana); + bool ok = false; + std::shared_ptr loaded = roundtrip_with_sentinel(orig, ok); + ASSERT_NE(loaded, nullptr); + EXPECT_NE(std::dynamic_pointer_cast(loaded), nullptr); + EXPECT_TRUE(orig->equal(*loaded)); + EXPECT_TRUE(ok) << "HNLDecay body not consumed on load"; +} + +TEST(InteractionSerialization, ElectroweakDecayRoundTrip) { + std::shared_ptr orig = std::make_shared( + std::set{ParticleType::TauMinus}); + bool ok = false; + std::shared_ptr loaded = roundtrip_with_sentinel(orig, ok); + ASSERT_NE(loaded, nullptr); + EXPECT_NE(std::dynamic_pointer_cast(loaded), nullptr); + EXPECT_TRUE(orig->equal(*loaded)); + EXPECT_TRUE(ok) << "ElectroweakDecay body not consumed on load"; +} + +TEST(InteractionSerialization, HNLDipoleDecayRoundTrip) { + std::shared_ptr orig = std::make_shared( + 0.1, std::vector{0.0, 1e-6, 0.0}, HNLDipoleDecay::ChiralNature::Majorana); + bool ok = false; + std::shared_ptr loaded = roundtrip_with_sentinel(orig, ok); + ASSERT_NE(loaded, nullptr); + EXPECT_NE(std::dynamic_pointer_cast(loaded), nullptr); + EXPECT_TRUE(orig->equal(*loaded)); + EXPECT_TRUE(ok) << "HNLDipoleDecay body not consumed on load"; +} + +TEST(InteractionSerialization, ElasticScatteringRoundTrip) { + std::shared_ptr orig = std::make_shared( + std::set{ParticleType::NuMu}); + bool ok = false; + std::shared_ptr loaded = roundtrip_with_sentinel(orig, ok); + ASSERT_NE(loaded, nullptr); + EXPECT_NE(std::dynamic_pointer_cast(loaded), nullptr); + EXPECT_TRUE(orig->equal(*loaded)); + EXPECT_TRUE(ok) << "ElasticScattering body not consumed on load"; +} + +TEST(InteractionSerialization, HNLDipoleFromTableRoundTrip) { + std::shared_ptr orig = std::make_shared( + 0.1, 1e-6, HNLDipoleFromTable::HelicityChannel::Conserving); + bool ok = false; + std::shared_ptr loaded = roundtrip_with_sentinel(orig, ok); + ASSERT_NE(loaded, nullptr); + EXPECT_NE(std::dynamic_pointer_cast(loaded), nullptr); + EXPECT_TRUE(orig->equal(*loaded)); + EXPECT_TRUE(ok) << "HNLDipoleFromTable body not consumed on load"; +} diff --git a/projects/interactions/public/SIREN/interactions/ElasticScattering.h b/projects/interactions/public/SIREN/interactions/ElasticScattering.h index 38cc95b1d..7870442ed 100644 --- a/projects/interactions/public/SIREN/interactions/ElasticScattering.h +++ b/projects/interactions/public/SIREN/interactions/ElasticScattering.h @@ -64,7 +64,7 @@ friend cereal::access; } } template - void load_and_construct(Archive & archive, cereal::construct & construct, std::uint32_t version) { + static void load_and_construct(Archive & archive, cereal::construct & construct, std::uint32_t version) { if(version == 0) { std::set _primary_types; archive(::cereal::make_nvp("PrimaryTypes", _primary_types)); diff --git a/projects/interactions/public/SIREN/interactions/ElectroweakDecay.h b/projects/interactions/public/SIREN/interactions/ElectroweakDecay.h index b68b3596e..37e39e708 100644 --- a/projects/interactions/public/SIREN/interactions/ElectroweakDecay.h +++ b/projects/interactions/public/SIREN/interactions/ElectroweakDecay.h @@ -96,7 +96,7 @@ friend cereal::access; } } template - void load_and_construct(Archive & archive, cereal::construct & construct, std::uint32_t version) { + static void load_and_construct(Archive & archive, cereal::construct & construct, std::uint32_t version) { if(version == 0) { std::set _primary_types; archive(::cereal::make_nvp("PrimaryTypes", _primary_types)); diff --git a/projects/interactions/public/SIREN/interactions/HNLDecay.h b/projects/interactions/public/SIREN/interactions/HNLDecay.h index 284a8df76..e9c0c2cd1 100644 --- a/projects/interactions/public/SIREN/interactions/HNLDecay.h +++ b/projects/interactions/public/SIREN/interactions/HNLDecay.h @@ -117,7 +117,7 @@ HNLDecay(): crundec(new CRunDec()){}; } } template - void load_and_construct(Archive & archive, cereal::construct & construct, std::uint32_t version) { + static void load_and_construct(Archive & archive, cereal::construct & construct, std::uint32_t version) { if(version == 0) { std::set _primary_types; double _hnl_mass; diff --git a/projects/interactions/public/SIREN/interactions/HNLDipoleDecay.h b/projects/interactions/public/SIREN/interactions/HNLDipoleDecay.h index 73b6d4623..d5c0e57cb 100644 --- a/projects/interactions/public/SIREN/interactions/HNLDipoleDecay.h +++ b/projects/interactions/public/SIREN/interactions/HNLDipoleDecay.h @@ -75,7 +75,7 @@ HNLDipoleDecay() {}; } } template - void load_and_construct(Archive & archive, cereal::construct & construct, std::uint32_t version) { + static void load_and_construct(Archive & archive, cereal::construct & construct, std::uint32_t version) { if(version == 0) { std::set _primary_types; double _hnl_mass; diff --git a/projects/interactions/public/SIREN/interactions/HNLDipoleFromTable.h b/projects/interactions/public/SIREN/interactions/HNLDipoleFromTable.h index f02504477..344cabec0 100644 --- a/projects/interactions/public/SIREN/interactions/HNLDipoleFromTable.h +++ b/projects/interactions/public/SIREN/interactions/HNLDipoleFromTable.h @@ -98,7 +98,7 @@ HNLDipoleFromTable() {}; } } template - void load_and_construct(Archive & archive, cereal::construct & construct, std::uint32_t version) { + static void load_and_construct(Archive & archive, cereal::construct & construct, std::uint32_t version) { if(version == 0) { bool _z_samp = true; bool _in_invGeV = true; From db997b68148765d1d8873d246d414429f8655e7b Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 14:44:18 -0500 Subject: [PATCH 76/93] Tighten charm-DIS code comments: concise, targeted, no development history Rewrite the comments introduced by the charm-DIS work to describe only the current code and its non-obvious rationale, dropping development-history framing, commit hashes, and plan-item ids. Comment-only. --- .../DetectorModelDegenerateDirection_TEST.cxx | 54 +++++++++---------- projects/injection/private/Weighter.cxx | 10 ++-- .../private/test/CharmSerialization_TEST.cxx | 28 +++++----- projects/interactions/CMakeLists.txt | 6 +-- .../interactions/private/CharmMesonDecay.cxx | 4 +- .../private/CharmMesonDecay3Body.cxx | 4 +- projects/interactions/private/DMesonELoss.cxx | 2 +- .../private/PythiaDISCrossSection.cxx | 4 +- .../private/QuarkDISFromSpline.cxx | 26 +++++---- .../private/pybindings/DMesonELoss.h | 4 +- .../private/test/CharmDISClosure_TEST.cxx | 12 ++--- .../test/CharmMesonDecay3Body_TEST.cxx | 11 ++-- .../private/test/CharmMesonDecay_TEST.cxx | 10 ++-- .../test/InteractionSerialization_TEST.cxx | 16 +++--- .../test/QuarkDISDensityContract_TEST.cxx | 6 +-- .../SIREN/interactions/CharmMesonDecay.h | 4 +- .../SIREN/interactions/CharmMesonDecay3Body.h | 4 +- projects/math/private/test/Vector3D_TEST.cxx | 17 +++--- .../utilities/public/SIREN/utilities/Random.h | 18 ++----- tests/python/test_quarkdis_slow_rescaling.py | 22 +++----- 20 files changed, 119 insertions(+), 143 deletions(-) diff --git a/projects/detector/private/test/DetectorModelDegenerateDirection_TEST.cxx b/projects/detector/private/test/DetectorModelDegenerateDirection_TEST.cxx index a005bc072..16803d911 100644 --- a/projects/detector/private/test/DetectorModelDegenerateDirection_TEST.cxx +++ b/projects/detector/private/test/DetectorModelDegenerateDirection_TEST.cxx @@ -1,28 +1,27 @@ -// Regression test for the degenerate trajectory-direction fix (commit 29e189f6). +// Degenerate trajectory-direction behavior for DetectorModel depth queries. // -// Root cause: DetectorModel computes a trajectory direction as (p1 - p0) in -// cartesian coordinates. When p0 and p1 are at Earth-scale coordinates and only +// DetectorModel computes a trajectory direction as (p1 - p0) in cartesian +// coordinates. When p0 and p1 are at Earth-scale coordinates and only // micrometers apart, the subtraction suffers catastrophic cancellation, so the -// resulting direction is unreliable. The old code either (a) normalized a -// near-zero vector (dividing by ~0 -> NaN, since Vector3D::normalize() had no -// zero-length guard) or (b) reached assert(std::abs(1.0 - std::abs(dot)) < 1e-6) -// with a non-unit direction and aborted. +// resulting direction is unreliable; for coincident points it is the zero +// vector. The required behavior in these cases: +// - Vector3D::normalize() must not divide by zero (no NaN/Inf). +// - The interaction-depth sub-threshold path returns the well-defined decay +// term (distance / total_decay_length) rather than normalizing an +// unreliable direction and tripping the unit-direction assert. +// - All depth queries must return finite, non-negative values and must not +// abort. // -// The fix guards Vector3D::normalize() against zero length and makes the -// interaction-depth sub-threshold guard return the well-defined decay term -// (distance / total_decay_length) instead of normalizing an unreliable -// direction. +// These tests construct the degenerate cases directly. They use a default +// (infinite-vacuum) DetectorModel, so they are fully self-contained and require +// no external data files. // -// These tests construct exactly the degenerate cases and assert the previously -// failing paths now return finite, sensible values and do not abort. They use a -// default (infinite-vacuum) DetectorModel, so they are fully self-contained and -// require no external data files. -// -// NOTE: the dot-product assert only fires in an assertions-enabled build -// (Debug / RelWithDebInfo). In a fully optimized NDEBUG build the regression -// would not abort even without the fix, but the finite/non-NaN return-value -// assertions below still exercise and lock in the corrected numeric behavior. +// NOTE: the unit-direction assert (std::abs(1.0 - std::abs(dot)) < 1e-6) only +// fires in an assertions-enabled build (Debug / RelWithDebInfo); in a fully +// optimized NDEBUG build it is compiled out. The finite/non-NaN return-value +// assertions below exercise and lock in the required numeric behavior in either +// build. #include #include @@ -57,7 +56,7 @@ bool IsFinite(double x) { // p0 and p1 are distinct (so the p0 == p1 early-return does NOT fire) but their // separation is below distance_threshold. GetInteractionDepthInCGS with non-empty // targets must short-circuit to distance/total_decay_length rather than -// normalizing the cancellation-garbage direction and tripping the dot assert. +// normalizing the cancellation-corrupted direction and tripping the unit assert. TEST(DetectorModelDegenerateDirection, InteractionDepthSubThresholdWithTargets) { DetectorModel A; // infinite vacuum sphere, no files @@ -86,7 +85,7 @@ TEST(DetectorModelDegenerateDirection, InteractionDepthSubThresholdWithTargets) EXPECT_TRUE(IsFinite(depth)) << "InteractionDepth must be finite, got " << depth; - // The fix returns the decay-only limit at sub-threshold distance, matching + // At sub-threshold distance the result is the decay-only limit, matching // the targets.empty() branch (continuity across the threshold). double expected = distance / total_decay_length; EXPECT_NEAR(expected, depth, std::abs(expected) * 1e-9 + 1e-30); @@ -120,9 +119,8 @@ TEST(DetectorModelDegenerateDirection, InteractionDepthSubThresholdDecayOnly) } // GetColumnDepthInCGS on a sub-threshold but distinct segment normalizes a tiny -// (magnitude ~1e-7) direction. Before the normalize zero-guard a truly zero -// difference produced NaN; here the difference is small-but-nonzero, so the call -// must complete without aborting and return a finite value. +// (magnitude ~1e-7) direction. The difference is small-but-nonzero, so the call +// must complete without aborting and return a finite, non-negative value. TEST(DetectorModelDegenerateDirection, ColumnDepthSubThresholdIsFinite) { DetectorModel A; @@ -165,9 +163,9 @@ TEST(DetectorModelDegenerateDirection, ParticleColumnDepthSubThresholdIsFinite) } // Exact-coincidence case (p0 == p1) at Earth-scale coordinates. Here (p1 - p0) -// is the zero vector. The early p0 == p1 guard returns the degenerate limit, but -// the Vector3D::normalize() zero-guard is the safety net should any path reach -// it. All depth queries must return 0 / finite and not produce NaN. +// is the zero vector. The early p0 == p1 guard returns the degenerate limit, and +// the Vector3D::normalize() zero-length guard protects any path that still +// normalizes it. All depth queries must return 0, finite, and not produce NaN. TEST(DetectorModelDegenerateDirection, CoincidentPointsReturnZeroFinite) { DetectorModel A; diff --git a/projects/injection/private/Weighter.cxx b/projects/injection/private/Weighter.cxx index 221f1787a..6e7bfb228 100644 --- a/projects/injection/private/Weighter.cxx +++ b/projects/injection/private/Weighter.cxx @@ -49,7 +49,7 @@ using detector::DetectorDirection; //--------------- void Weighter::Initialize() { - // Idempotent: clear any previously-built weighters so Initialize() can be + // Idempotent: clear any weighters from a prior Initialize() so this can be // called again after LoadWeighter() or after injectors are overwritten. primary_process_weighters.clear(); secondary_process_weighter_maps.clear(); @@ -200,10 +200,10 @@ void Weighter::SaveWeighter(std::string const & filename) const { void Weighter::LoadWeighter(std::string const & filename) { std::ifstream is(filename+".siren_weighter", std::ios::binary); ::cereal::BinaryInputArchive archive(is); - // Read the same members SaveWeighter (Weighter::save) wrote, in the same - // order; the polymorphic Injector / PhysicalProcess (and the cross sections - // and decays they hold) are reconstructed via their registered cereal - // load hooks. Then rebuild the per-process weighters. + // Read members in the same order Weighter::save writes them; the polymorphic + // Injector / PhysicalProcess (and the cross sections and decays they hold) + // are reconstructed via their registered cereal load hooks. Then rebuild the + // per-process weighters. archive(::cereal::make_nvp("Injectors", injectors)); archive(::cereal::make_nvp("DetectorModel", detector_model)); archive(::cereal::make_nvp("PrimaryPhysicalProcess", primary_physical_process)); diff --git a/projects/injection/private/test/CharmSerialization_TEST.cxx b/projects/injection/private/test/CharmSerialization_TEST.cxx index 68f60d461..025fe775a 100644 --- a/projects/injection/private/test/CharmSerialization_TEST.cxx +++ b/projects/injection/private/test/CharmSerialization_TEST.cxx @@ -1,17 +1,15 @@ // Serialization round-trip tests for the charm-DIS decay classes and the Weighter. // -// These guard two previously-broken behaviors: -// 1. CharmMesonDecay / CharmMesonDecay3Body are default-constructible but used -// cereal save() + load_and_construct(). cereal does not invoke -// load_and_construct for a default-constructible type, so the serialized -// body was never read on load -- the object survived only because its -// primary_types set is a fixed const member, while every byte of the body -// was left in the stream, corrupting whatever followed. They now use a -// member load(); the trailing-sentinel assertions below catch any -// regression to a body-skipping load. -// 2. Weighter::LoadWeighter was a stub that printed "not yet supported" and -// called exit(0). It now deserializes the weighter; the round-trip reaches -// its assertions (it would have killed the test process before). +// Two invariants are enforced: +// 1. CharmMesonDecay / CharmMesonDecay3Body must consume their entire +// serialized body on load. Because both are default-constructible, cereal +// uses a member load() rather than load_and_construct (it does not invoke +// load_and_construct for a default-constructible type). The trailing +// sentinel after each decay reads back correctly only if the body is fully +// consumed, so it directly detects a load that skips the body. +// 2. A Weighter holding a charm decay process must survive SaveWeighter +// followed by reconstruction through the filename constructor (LoadWeighter) +// and re-serialize byte-identically. #include #include @@ -45,7 +43,7 @@ using siren::detector::DetectorModel; namespace { // Round-trip a polymorphic Decay through a binary archive followed by a sentinel // int. The sentinel reads back correctly only if the decay body was fully -// consumed on load -- i.e. it directly detects the body-skip serialization bug. +// consumed on load -- i.e. it detects a load that leaves bytes in the stream. std::shared_ptr roundtrip_decay_with_sentinel(std::shared_ptr orig, bool & sentinel_ok) { const int kSentinel = 0x5A5A5A; @@ -126,8 +124,8 @@ TEST(CharmSerialization, WeighterWithCharmDecaySaveLoad) { ASSERT_NO_THROW(w.SaveWeighter(base)); // writes .siren_weighter - // The filename constructor calls LoadWeighter (previously a stub that - // exit(0)'d). Reaching the next statement at all proves loading is enabled. + // The filename constructor calls LoadWeighter to reconstruct the weighter + // from the serialized file. std::unique_ptr w2; ASSERT_NO_THROW(w2.reset(new Weighter(no_injectors, base))); ASSERT_NE(w2, nullptr); diff --git a/projects/interactions/CMakeLists.txt b/projects/interactions/CMakeLists.txt index 8bde4c528..45c480015 100644 --- a/projects/interactions/CMakeLists.txt +++ b/projects/interactions/CMakeLists.txt @@ -113,9 +113,9 @@ package_add_test(UnitTest_CharmMesonDecay3Body ${PROJECT_SOURCE_DIR}/projects/in # fragmentation pdf/cdf). package_add_test(UnitTest_QuarkDISDensityContract ${PROJECT_SOURCE_DIR}/projects/interactions/private/test/QuarkDISDensityContract_TEST.cxx) -# Serialization round-trips for the interaction classes whose load_and_construct -# was made static (HNLDecay, ElectroweakDecay, HNLDipoleDecay, ElasticScattering, -# HNLDipoleFromTable). +# Serialization round-trips for the interaction classes that use a static +# load_and_construct (HNLDecay, ElectroweakDecay, HNLDipoleDecay, +# ElasticScattering, HNLDipoleFromTable). package_add_test(UnitTest_InteractionSerialization ${PROJECT_SOURCE_DIR}/projects/interactions/private/test/InteractionSerialization_TEST.cxx) # Real PythiaDISCrossSection charm-DIS closure/normalization regression test. diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx index b01268b57..46d496747 100644 --- a/projects/interactions/private/CharmMesonDecay.cxx +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -760,8 +760,8 @@ double CharmMesonDecay::VAWeightAngleAverage(double mD, double mK, double ml, do // exact quadratic. clamp(q, 0, M) = max(0, q) - max(0, q - M) turns the // accept-reject ceiling into two positive-part integrals, each closed form. // This reproduces SampleFinalState's accepted density exactly (weighting - // closure) with no quadrature error; the equivalent numeric quadrature is - // kept as the regression oracle VAWeightAngleAverageMatchesNumericReference. + // closure) with no quadrature error; the same integral is also evaluated + // numerically in the unit tests as a cross-check. double mnu = 0.0; double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) * (mD + mK - m23) * (mD - mK + m23)) / mD; diff --git a/projects/interactions/private/CharmMesonDecay3Body.cxx b/projects/interactions/private/CharmMesonDecay3Body.cxx index 93ccd2b04..4f6430b01 100644 --- a/projects/interactions/private/CharmMesonDecay3Body.cxx +++ b/projects/interactions/private/CharmMesonDecay3Body.cxx @@ -574,8 +574,8 @@ double CharmMesonDecay3Body::VAWeightAngleAverage(double mD, double mK, double m // c = cos(theta_lepton) after the boost to the D rest frame, so the positive // and clipped parts integrate in closed form via // clamp(q, 0, M) = max(0, q) - max(0, q - M). Matches SampleFinalState's - // accepted density exactly (closure) with no quadrature error; the numeric - // quadrature lives in VAWeightAngleAverageMatchesNumericReference. + // accepted density exactly (closure) with no quadrature error; the same + // integral is also evaluated numerically in the unit tests as a cross-check. double mnu = 0.0; double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) * (mD + mK - m23) * (mD - mK + m23)) / mD; diff --git a/projects/interactions/private/DMesonELoss.cxx b/projects/interactions/private/DMesonELoss.cxx index f439d40a0..533ed2d42 100644 --- a/projects/interactions/private/DMesonELoss.cxx +++ b/projects/interactions/private/DMesonELoss.cxx @@ -198,7 +198,7 @@ void DMesonELoss::SampleFinalState(dataclasses::CrossSectionDistributionRecord& // truncated Gaussian that DifferentialCrossSection/FinalStateProbability // normalize over the same interval (closure). z < z_min_ would otherwise let // the D meson GAIN energy (final_energy > primary_energy). The kinematic cut - // final_energy^2 >= Dmass^2 is kept as a defensive guard. + // final_energy^2 >= Dmass^2 is a defensive guard. accept = (z >= z_min_) && (z <= z_max_) && (pow(final_energy, 2) - pow(Dmass, 2) >= 0); } while (!accept); diff --git a/projects/interactions/private/PythiaDISCrossSection.cxx b/projects/interactions/private/PythiaDISCrossSection.cxx index a815a2679..094e51666 100644 --- a/projects/interactions/private/PythiaDISCrossSection.cxx +++ b/projects/interactions/private/PythiaDISCrossSection.cxx @@ -254,7 +254,7 @@ void PythiaDISCrossSection::InitializeSignatures() { // the cbar quark fragments to Dbar0/D-/Ds-. SampleFinalState writes Pythia's // actual produced PID into the signature's meson slot, so the registered set // must include the correct charge to keep weighter signature lookups in range - // (otherwise event_weight comes out NaN -- see fix in this commit). + // (otherwise event_weight would be NaN). // TODO: Add Lambda_c (4122) support. bool is_antineutrino = (primary_type == siren::dataclasses::ParticleType::NuEBar || @@ -440,7 +440,7 @@ std::vector PythiaDISCrossSection::DensityVariables() const { } // ====================================================================== -// Pythia initialization and SampleFinalState -- the core new logic +// Pythia initialization and SampleFinalState // ====================================================================== void PythiaDISCrossSection::InitializePythia(double E_nu, int target_pdg) const { diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index ce693e389..79669428b 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -51,7 +51,7 @@ inline double slowRescalingW2(double xi, double y, double E, double M, double mc ///\brief Slow-rescaling kinematic check. /// -/// Replaces the old (x_BJ, y) check. Cuts: +/// Acceptance cuts: /// xi in [1e-9, 1], y in [1e-9, 1 - m_lep/E], /// Q^2 = 2 M E y xi - m_c^2 > 0 (charm threshold), /// W^2 = M^2 + 2 M E y (1-xi) + m_c^2 > (M + M_D0)^2. @@ -70,11 +70,11 @@ bool kinematicallyAllowed(double xi, double y, double E, double M, double m_lep) if (W2 <= (M + Mch) * (M + Mch)) return false; // Transverse-momentum balance: the exchanged q must have a real transverse - // component pqy in the lab frame. This is the SAME q-decomposition the sampler - // uses (SampleFinalState), so adding it here makes kinematicallyAllowed the - // single predicate shared by the sampler proposal loops and by - // DifferentialCrossSection/FinalStateProbability. Without it the density - // (dxs/txs) is nonzero on points the sampler must reject (pqy^2 < 0), + // component pqy in the lab frame. This is the same q-decomposition the sampler + // uses (SampleFinalState), so kinematicallyAllowed is the single predicate + // shared by the sampler proposal loops and by + // DifferentialCrossSection/FinalStateProbability. Without this check the + // density (dxs/txs) would be nonzero on points the sampler rejects (pqy^2 < 0), // breaking Sample==Density closure and silently biasing low-Bjorken-x events. // // Primary is a neutrino here (InitializeSignatures enforces isNeutrino), so @@ -805,10 +805,9 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR double Q2 = slowRescalingQ2(final_xi, final_y, E1_lab, target_mass_, siren::utilities::Constants::charmMass); - // Scale-free closed form for the exchanged q decomposition (replaces the old - // /10 precision-rescaling loop, which was unsound: slowRescalingQ2 holds the - // target/charm masses fixed, so Q2 is NOT homogeneous of degree 2 under the - // scaling and the loop never produced a valid Q2). + // Closed form for the exchanged q decomposition. A uniform momentum rescaling + // cannot be used to recompute Q2 here: slowRescalingQ2 holds the target/charm + // masses fixed, so Q2 is not homogeneous of degree 2 under the scaling. // // p1x_lab is the 3-momentum MAGNITUDE P1 = |p1_lab| (not an x-component). // The naive expressions @@ -894,7 +893,7 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR // Save final state kinematics. // - // NOTE (T3): the sampled momenta are written into the record's + // NOTE: the sampled momenta are written into the record's // SecondaryParticleRecord vector (record.GetSecondaryParticleRecords()), NOT // directly into an InteractionRecord's secondary_momenta. To obtain a finalized // InteractionRecord with populated secondary_momenta, the caller must run @@ -902,8 +901,7 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR // output record whose signature is set. Building an InteractionRecord by hand // with empty/zero secondary_momenta and feeding it back to // DifferentialCrossSection makes the primary-momentum Q2 path compute Q2 <= 0, - // forcing the stored-(xi,y) fallback branch; that is a caller mistake, not a - // limitation of SampleFinalState, which populates the state correctly here. + // forcing the stored-(xi,y) fallback branch. std::vector & secondaries = record.GetSecondaryParticleRecords(); siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[lepton_index]; siren::dataclasses::SecondaryParticleRecord & hadron = secondaries[hadron_index]; @@ -1047,7 +1045,7 @@ double QuarkDISFromSpline::FragmentationFraction(siren::dataclasses::Particle::P // on the D kinematics. Biasing the D kinematics is NOT supported and would produce // incorrect weights. // -// NORMALIZATION CONTRACT (P8): FinalStateProbability = dxs/txs is a normalized +// NORMALIZATION CONTRACT: FinalStateProbability = dxs/txs is a normalized // kinematic density ONLY if the external 1-D total-xs spline (txs) equals the // integral of the differential spline (dxs) over the SAME truncated slow-rescaling // domain: xi in [xiMin(E),1], y in [yMin(E),yMax(E)] with identical charm-threshold diff --git a/projects/interactions/private/pybindings/DMesonELoss.h b/projects/interactions/private/pybindings/DMesonELoss.h index 56c554a64..ce88c6463 100644 --- a/projects/interactions/private/pybindings/DMesonELoss.h +++ b/projects/interactions/private/pybindings/DMesonELoss.h @@ -7,8 +7,8 @@ #include #include "../../public/SIREN/interactions/CrossSection.h" -// Self-include the public class header so this TU does not rely on -// interactions.cxx pulling DMesonELoss.h in before this pybinding header. +// Include the public class header directly so this pybinding header is +// self-contained regardless of include order in its includer. #include "../../public/SIREN/interactions/DMesonELoss.h" #include "../../../dataclasses/public/SIREN/dataclasses/Particle.h" #include "../../../dataclasses/public/SIREN/dataclasses/InteractionRecord.h" diff --git a/projects/interactions/private/test/CharmDISClosure_TEST.cxx b/projects/interactions/private/test/CharmDISClosure_TEST.cxx index 0c11c461a..784a49959 100644 --- a/projects/interactions/private/test/CharmDISClosure_TEST.cxx +++ b/projects/interactions/private/test/CharmDISClosure_TEST.cxx @@ -41,9 +41,9 @@ using namespace siren::dataclasses; namespace { // Mock reproducing PythiaDISCrossSection's relevant semantics. -// apply_ff : multiply by FragmentationFraction per signature (the fix / QuarkDIS) +// apply_ff : multiply by FragmentationFraction per signature (as in QuarkDISFromSpline) // override_afs : short-circuit TotalCrossSectionAllFinalStates to TotalCrossSection -// (the spurious f1751c6b override) +// instead of the base per-signature sum class MockCharmXS : public CrossSection { public: bool apply_ff; @@ -164,10 +164,10 @@ TEST(CharmDISClosure, OverrideRemovedClosesButOvercountsThreeX) { } } -// The fix (override removed + fragmentation fraction in TotalCrossSection, with -// the FFs renormalized to sum to 1.0): the two sides agree AND both equal the -// full inclusive charm cross section sigma -- partitioned across the three D -// species, not triple-counted and not the 2%-deficient 0.98*sigma. +// With the fragmentation fraction applied per signature in TotalCrossSection +// (and the FFs renormalized to sum to 1.0), the generation-side and physical- +// side inclusive charm cross sections must agree AND both equal the full +// inclusive sigma -- partitioned across the three D species, not triple-counted. TEST(CharmDISClosure, FragmentationFractionRestoresPhysicalNormalization) { MockCharmXS xs(/*ff=*/true, /*override=*/false); // FF sum = (0.6 + 0.23 + 0.15) / 0.98 == 1.0 (renormalized), so the diff --git a/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx b/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx index 3353c9414..fa176a7f7 100644 --- a/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx +++ b/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx @@ -246,7 +246,7 @@ TEST(CharmMesonDecay3Body, TotalDecayWidthAndBranchingSums) { bad_primary.signature.secondary_types = {ParticleType::Hadrons}; EXPECT_THROW(decay.TotalDecayWidthForFinalState(bad_primary), std::runtime_error); - // Matched primary (D0) but an unimplemented set of secondaries throws (R4). + // Matched primary (D0) but an unimplemented set of secondaries throws. InteractionRecord bad_secondaries; bad_secondaries.signature.primary_type = ParticleType::D0; bad_secondaries.signature.target_type = ParticleType::Decay; @@ -345,11 +345,10 @@ TEST(CharmMesonDecay3Body, FinalStateProbabilityClosure) { // --- Analytic angle-average matches a numeric quadrature oracle ------------ // -// The weighting code uses the CLOSED-FORM CharmMesonDecay3Body:: -// VAWeightAngleAverage. This is the "shunted integral": a high-resolution -// numeric quadrature of the identical clamped V-A weight (same rk::P4 boosts as -// SampleFinalState) that the analytic form must reproduce, guarding against any -// algebra error in the closed form. +// The weighting code uses the closed-form CharmMesonDecay3Body:: +// VAWeightAngleAverage. The closed form must reproduce a high-resolution numeric +// quadrature of the identical clamped V-A weight, evaluated with the same rk::P4 +// boosts SampleFinalState uses. namespace { double numericVAWeightAngleAverage3B(double mD, double mK, double ml, double m23) { double mnu = 0.0; diff --git a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx index c198db8e9..ff4470d15 100644 --- a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx +++ b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx @@ -239,7 +239,7 @@ TEST(CharmMesonDecay, DiffDecayWidthComparison) { } // Pin the per-meson pole parameter alpha used by FormFactorFromRecord: - // 0.50 for D0, 0.44 for D+ (both intentional; not a bug to "correct"). + // 0.50 for D0, 0.44 for D+. InteractionRecord d0rec; d0rec.signature = sig; d0rec.primary_mass = mD; @@ -439,11 +439,9 @@ TEST(CharmMesonDecay, UnsupportedSignaturesThrow) { // --- Test 5: analytic angle-average matches a numeric quadrature oracle ----- // // The weighting code (FinalStateProbability via SampledQ2Density) uses the -// CLOSED-FORM CharmMesonDecay::VAWeightAngleAverage. This test is the "shunted -// integral": a high-resolution numeric quadrature of the identical clamped V-A -// weight, evaluated with the SAME rk::P4 boosts SampleFinalState uses, that the -// analytic form must reproduce. It guards against any future algebra error in -// the closed form (the kind that produced the original closure break). +// closed-form CharmMesonDecay::VAWeightAngleAverage. The closed form must +// reproduce a high-resolution numeric quadrature of the identical clamped V-A +// weight, evaluated with the same rk::P4 boosts SampleFinalState uses. namespace { double numericVAWeightAngleAverage(double mD, double mK, double ml, double m23) { double mnu = 0.0; diff --git a/projects/interactions/private/test/InteractionSerialization_TEST.cxx b/projects/interactions/private/test/InteractionSerialization_TEST.cxx index 2da3134d7..4abeaf31b 100644 --- a/projects/interactions/private/test/InteractionSerialization_TEST.cxx +++ b/projects/interactions/private/test/InteractionSerialization_TEST.cxx @@ -1,13 +1,11 @@ -// Serialization round-trip tests for the interaction classes whose -// load_and_construct was a NON-STATIC member. cereal only recognizes a STATIC +// Serialization round-trip tests for non-default-constructible interaction +// classes that rely on load_and_construct. cereal only recognizes a STATIC // load_and_construct (or a non-member specialization); a non-static one is -// silently ignored, so cereal default-constructed the object and never read the -// serialized body -- corrupting everything that followed it in the archive. -// These classes were changed to `static void load_and_construct`; the trailing -// sentinel below reads back correctly only if the body is fully consumed, so it -// directly guards against a regression to a body-skipping load. (DarkNewsDecay -// is intentionally excluded: it is abstract / python-backed and cannot be -// constructed by cereal here.) +// silently ignored, leaving the serialized body unread and corrupting whatever +// follows it in the archive. The trailing sentinel after each object reads back +// correctly only if the body is fully consumed, so it directly detects a load +// that skips the body. (DarkNewsDecay is intentionally excluded: it is abstract +// / python-backed and cannot be constructed by cereal here.) #include #include diff --git a/projects/interactions/private/test/QuarkDISDensityContract_TEST.cxx b/projects/interactions/private/test/QuarkDISDensityContract_TEST.cxx index bd9dfeb3b..b2f014849 100644 --- a/projects/interactions/private/test/QuarkDISDensityContract_TEST.cxx +++ b/projects/interactions/private/test/QuarkDISDensityContract_TEST.cxx @@ -1,9 +1,9 @@ /** * Contract test for QuarkDISFromSpline (slow-rescaling charm DIS sampler). * - * P6 finding: SampleFinalState samples (xi, y) AND an independently-sampled - * fragmentation z and a uniform azimuth phi that set the D-meson momentum, but - * the advertised density (DensityVariables / FinalStateProbability / + * Invariant under test: SampleFinalState samples (xi, y) AND an independently- + * sampled fragmentation z and a uniform azimuth phi that set the D-meson + * momentum, but the advertised density (DensityVariables / FinalStateProbability / * DifferentialCrossSection) accounts for (xi, y) only. The omitted z/phi factors * cancel in the weight ratio ONLY in the standard unbiased configuration (the * same cross-section object supplies both the injection and physical densities diff --git a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h index bd62e3715..e2f905c6b 100644 --- a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h +++ b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h @@ -49,8 +49,8 @@ friend cereal::access; virtual bool equal(Decay const & other) const override; static double particleMass(siren::dataclasses::ParticleType particle); // Analytic angle-average of the accepted V-A weight (q^2 density factor). - // Public so the closure/regression test can check it against a numeric - // quadrature oracle; it is a pure function of the decay masses and m23. + // Public so the unit tests can cross-check it against a numeric quadrature; + // it is a pure function of the decay masses and m23. double VAWeightAngleAverage(double mD, double mK, double ml, double m23) const; double TotalDecayWidth(dataclasses::InteractionRecord const &) const override; double TotalDecayWidth(siren::dataclasses::Particle::ParticleType primary) const override; diff --git a/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h b/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h index fd22bcbff..9b8111746 100644 --- a/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h +++ b/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h @@ -60,8 +60,8 @@ friend cereal::access; virtual bool equal(Decay const & other) const override; static double particleMass(siren::dataclasses::ParticleType particle); // Analytic angle-average of the accepted V-A weight (q^2 density factor). - // Public so the closure/regression test can check it against a numeric - // quadrature oracle; pure function of the decay masses and m23. + // Public so the unit tests can cross-check it against a numeric quadrature; + // pure function of the decay masses and m23. double VAWeightAngleAverage(double mD, double mK, double ml, double m23) const; double TotalDecayWidth(dataclasses::InteractionRecord const &) const override; double TotalDecayWidth(siren::dataclasses::Particle::ParticleType primary) const override; diff --git a/projects/math/private/test/Vector3D_TEST.cxx b/projects/math/private/test/Vector3D_TEST.cxx index 4eb18f798..b3dc31a76 100644 --- a/projects/math/private/test/Vector3D_TEST.cxx +++ b/projects/math/private/test/Vector3D_TEST.cxx @@ -218,11 +218,11 @@ TEST(Normalize, Operator) EXPECT_TRUE(B != C); } -// Regression test for the degenerate trajectory-direction fix (commit 29e189f6). -// Before the fix, Vector3D::normalize() divided every component by a zero length -// for a zero vector, producing NaN. This is the root cause that propagated up -// into DetectorModel where a degenerate (p1 - p0) direction was normalized and -// then asserted to be unit length. The guard now leaves a zero vector unchanged. +// Normalizing the zero vector must not divide by zero: the result must stay +// finite (no NaN/Inf) and the vector is left unchanged at magnitude 0. A unit +// direction cannot be defined for a zero-length vector, so leaving it as-is is +// the well-defined contract callers (e.g. a degenerate p1 - p0 direction in +// DetectorModel) rely on. TEST(Normalize, ZeroVectorDoesNotProduceNaN) { Vector3D Z; @@ -254,9 +254,10 @@ TEST(Normalize, ZeroVectorNormalizedIsFinite) EXPECT_DOUBLE_EQ(0.0, N.magnitude()); } -// A near-zero but genuinely tiny difference of two Earth-scale coordinates (the -// pattern that arises from p1 - p0 with catastrophic cancellation) must still -// normalize to a finite, unit-length vector when the difference is nonzero. +// A genuinely tiny but nonzero difference of two Earth-scale coordinates must +// still normalize to a finite, unit-length vector. This is the boundary case +// just above the zero-length guard: the result must be a true unit vector, not +// left unchanged. TEST(Normalize, TinyEarthScaleDifferenceIsFiniteUnit) { // Two points separated by 1e-7 m built at Earth-scale x ~ 6.371e6 m. diff --git a/projects/utilities/public/SIREN/utilities/Random.h b/projects/utilities/public/SIREN/utilities/Random.h index 149d17607..e33d4599b 100644 --- a/projects/utilities/public/SIREN/utilities/Random.h +++ b/projects/utilities/public/SIREN/utilities/Random.h @@ -59,19 +59,11 @@ class SIREN_random{ private: uint64_t seed; - // Previously used std::default_random_engine (minstd_rand0 on GCC), - // a linear congruential generator with only ~2.1 billion states - // (period 2^31-2). With ~500 RNG draws per event and thousands of - // seeds each generating 10k events, the birthday paradox causes - // frequent internal state collisions across seeds, producing - // identical event sequences. Switched to Mersenne Twister - // (period 2^19937-1) to eliminate cross-seed duplicates. - // REPRODUCIBILITY NOTE: because the engine changed, a given fixed - // seed now produces a DIFFERENT deterministic sequence than the old - // std::default_random_engine. Serialization is unchanged (only the - // seed is archived, version 0), so existing serialized objects still - // load; but any cached samples or golden baselines keyed to a seed - // must be regenerated. Do not revert the engine to restore old output. + // 64-bit Mersenne Twister (period 2^19937-1). The period must far exceed + // (number of seeds) x (draws per event) x (events per seed) so that distinct + // seeds never collide into overlapping draw sequences. A given seed + // deterministically fixes the entire sequence; only the seed is serialized + // (version 0), so any cached samples or golden baselines are keyed to the seed. std::mt19937_64 configuration; std::uniform_real_distribution generator; }; diff --git a/tests/python/test_quarkdis_slow_rescaling.py b/tests/python/test_quarkdis_slow_rescaling.py index bf263dd4c..ffbaa007d 100644 --- a/tests/python/test_quarkdis_slow_rescaling.py +++ b/tests/python/test_quarkdis_slow_rescaling.py @@ -1,10 +1,5 @@ """Slow-rescaling (xi, y) sampling tests for QuarkDISFromSpline. -These were previously two orphan smoke scripts under tests/slow_rescaling/ -(smoke_quarkdis_100.py, smoke_quarkdis_10k.py) that were never collected by -pytest (testpaths = tests/python) and aborted the session with module-level -sys.exit() calls. They are now real, collectable pytest tests. - The charm slow-rescaling FITS splines are LHAPDF-derived and not committed to the repository, so every spline-dependent test is gated behind the SIREN_CHARM_SPLINE_DIR environment variable. Point it at a directory containing @@ -17,7 +12,7 @@ Number of events for the differential-positivity test is configurable via SIREN_CHARM_NEVENTS (default 2000, kept modest so the suite stays fast; set it -to 10000 to reproduce the original 10k smoke run). +to 10000 for a heavier run). """ import math import os @@ -47,7 +42,7 @@ MAX_RETRIES = 100 # --------------------------------------------------------------------------- -# Spline-file gating (resolves the old hardcoded /n/holylfs05 cluster path) +# Spline-file gating: resolve spline paths from SIREN_CHARM_SPLINE_DIR. # --------------------------------------------------------------------------- _SPLINE_DIR = os.environ.get("SIREN_CHARM_SPLINE_DIR") _DIFF_FILE = ( @@ -226,13 +221,12 @@ def test_quarkdis_kinematic_bounds(charm_xs, signature, rng): def test_quarkdis_differential_positive(charm_xs, signature, rng): """Re-evaluate the spline on finalized records via the production path. - This drives the REAL weighting density: SampleFinalState populates the - CrossSectionDistributionRecord, cdr.finalize(ir_out) materializes the - secondary momenta, and DifferentialCrossSection(ir_out) is evaluated on the - finalized record. That exercises the primary-momentum Q2 branch of - DifferentialCrossSection -- exactly the path the Weighter runs -- instead - of the deliberately-broken zero-momenta fallback used by the old smoke - script. We assert a high finite-positive fraction. + This drives the weighting density exactly as the Weighter does: + SampleFinalState populates the CrossSectionDistributionRecord, + cdr.finalize(ir_out) materializes the secondary momenta, and + DifferentialCrossSection(ir_out) is evaluated on the finalized record, + which takes the primary-momentum Q2 branch. We assert a high + finite-positive fraction. """ import siren.dataclasses From d0b2ddeda7ea38a906b782794558b0dcbf7ca110 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 15:02:27 -0700 Subject: [PATCH 77/93] DMesonELoss: apply the energy-dependent kinematic cut in the density (closure at all energies) DifferentialCrossSection normalized the inelasticity Gaussian over the fixed [z_min_, z_max_], but SampleFinalState also enforces final_energy >= mD (z <= 1 - mD/E), so the density was mis-normalized when that cut binds. Normalize over [z_min_, min(z_max_, 1 - mD/E)] so Sample == Density at every energy; z_max_ becomes 1 and kinematics sets the upper bound. --- projects/interactions/private/DMesonELoss.cxx | 31 ++-- .../private/test/DMesonELoss_TEST.cxx | 134 ++++++++++-------- .../public/SIREN/interactions/DMesonELoss.h | 14 +- 3 files changed, 96 insertions(+), 83 deletions(-) diff --git a/projects/interactions/private/DMesonELoss.cxx b/projects/interactions/private/DMesonELoss.cxx index 533ed2d42..1629e1580 100644 --- a/projects/interactions/private/DMesonELoss.cxx +++ b/projects/interactions/private/DMesonELoss.cxx @@ -110,32 +110,33 @@ double DMesonELoss::TotalCrossSection(siren::dataclasses::Particle::ParticleType } double DMesonELoss::DifferentialCrossSection(dataclasses::InteractionRecord const & interaction) const { - rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); - double primary_energy; - rk::P4 p1_lab; - primary_energy = interaction.primary_momentum[0]; - p1_lab = p1; - + double primary_energy = interaction.primary_momentum[0]; + double Dmass = interaction.primary_mass; double final_energy = interaction.secondary_momenta[0][0]; double z = 1 - final_energy / primary_energy; - // The density is the truncated Gaussian in z normalized over [z_min_, z_max_]. - // Zero out-of-support records so the density support matches the sampler support - // (the sampler rejects z outside [z_min_, z_max_] as well). This keeps closure - // even for externally-constructed records with z < 0 (D meson gaining energy). - if(z < z_min_ || z > z_max_) { + // The density support MUST match the sampler's realized support, or + // FinalStateProbability is mis-normalized (closure break, worst at low primary + // energy). SampleFinalState accepts z in [z_min_, z_max_] AND enforces the + // energy-dependent kinematic cut final_energy >= Dmass, i.e. z <= 1 - Dmass/E. + // Apply the same cut here and normalize the Gaussian over the identical + // [z_min_, z_hi] interval. z_hi collapses below z_min_ for sub-threshold + // primaries (E <= Dmass), where there is no valid final state. + double z_kin = 1.0 - Dmass / primary_energy; // kinematic upper limit + double z_hi = (z_max_ < z_kin) ? z_max_ : z_kin; + if(z_hi <= z_min_ || z < z_min_ || z > z_hi) { return 0.0; } - // now normalize the gaussian + // normalize the gaussian over the realized support [z_min_, z_hi] double total_xsec = TotalCrossSection(interaction.signature.primary_type, primary_energy); double z0 = 0.56; double sigma = 0.2; - std::function integrand = [&] (double z) -> double { - return exp(-(pow(z - z0, 2))/(2 * pow(sigma, 2))); + std::function integrand = [&] (double zz) -> double { + return exp(-(pow(zz - z0, 2))/(2 * pow(sigma, 2))); }; - double unnormalized = siren::utilities::rombergIntegrate(integrand, z_min_, z_max_); + double unnormalized = siren::utilities::rombergIntegrate(integrand, z_min_, z_hi); double normalization = total_xsec / unnormalized; double diff_xsec = normalization * exp(-(pow(z - z0, 2))/(2 * pow(sigma, 2))); diff --git a/projects/interactions/private/test/DMesonELoss_TEST.cxx b/projects/interactions/private/test/DMesonELoss_TEST.cxx index d73c68e55..ee2d3c29e 100644 --- a/projects/interactions/private/test/DMesonELoss_TEST.cxx +++ b/projects/interactions/private/test/DMesonELoss_TEST.cxx @@ -26,6 +26,7 @@ * the advertised density). */ #include +#include #include #include #include @@ -192,77 +193,84 @@ TEST(DMesonELoss, SubThresholdThrows) { // --- Test 5: sampled-z density matches FinalStateProbability ---------------- -TEST(DMesonELoss, ZDensityClosure) { +TEST(DMesonELoss, ZDensityClosureAcrossEnergies) { DMesonELoss xs; auto sig = xs.GetPossibleSignaturesFromParents(ParticleType::D0, ParticleType::PPlus)[0]; - double E_D = 1000.0; // well above threshold, so the truncation is the only cut auto rng = std::make_shared(); + const double mD = Constants::D0Mass; + const double zlo = 0.001; // == z_min_ - // FinalStateProbability returns dxs/txs == normalized truncated Gaussian in - // z over [z_min_, z_max_]. Histogram z = 1 - E_out/E_D and compare the - // empirical pdf to FinalStateProbability evaluated at each bin center. - const double zlo = 0.001, zhi = 0.999; - const int NB = 20; - const double bw = (zhi - zlo) / NB; - std::vector counts(NB, 0); - const int N = 200000; + // Span low to high primary energy. At low E the kinematic cut z <= 1 - mD/E + // truncates the Gaussian well below its mean (z0 = 0.56) -- exactly where a + // fixed-interval density (normalizing over [z_min_, z_max_] regardless of E) + // mis-normalizes and breaks closure. The density now applies the same + // energy-dependent cut, so Sample == Density at every energy. + for (double E_D : {5.0, 10.0, 50.0, 200.0, 1000.0, 3000.0}) { + const double z_kin = 1.0 - mD / E_D; // kinematic upper limit + const double zhi = std::min(1.0, z_kin); // == min(z_max_, z_kin) + ASSERT_GT(zhi, zlo) << "E_D=" << E_D; + const double p_D = std::sqrt(E_D * E_D - mD * mD); - for (int i = 0; i < N; ++i) { - InteractionRecord rec = make_d0_record(sig, E_D); - CrossSectionDistributionRecord cdr(rec); - xs.SampleFinalState(cdr, rng); - cdr.Finalize(rec); - double E_out = rec.secondary_momenta[0][0]; - double z = 1.0 - E_out / E_D; - // All accepted samples lie in [z_min_, z_max_]. - EXPECT_GE(z, zlo - 1e-9); - EXPECT_LE(z, zhi + 1e-9); - int b = (int)((z - zlo) / bw); - if (b < 0) b = 0; - if (b >= NB) b = NB - 1; - counts[b]++; - } + const int NB = 20; + const double bw = (zhi - zlo) / NB; + std::vector counts(NB, 0); + const int N = 200000; + for (int i = 0; i < N; ++i) { + InteractionRecord rec = make_d0_record(sig, E_D); + CrossSectionDistributionRecord cdr(rec); + xs.SampleFinalState(cdr, rng); + cdr.Finalize(rec); + double z = 1.0 - rec.secondary_momenta[0][0] / E_D; + // The sampler must respect the same support the density advertises. + EXPECT_GE(z, zlo - 1e-9) << "E_D=" << E_D; + EXPECT_LE(z, zhi + 1e-9) << "E_D=" << E_D; + int b = (int)((z - zlo) / bw); + if (b < 0) b = 0; + if (b >= NB) b = NB - 1; + counts[b]++; + } - // Build a record at a chosen z and read back the normalized z-density. - auto fsp_at_z = [&](double z) -> double { - double E_out = E_D * (1.0 - z); - double mD = Constants::D0Mass; - double p_out = std::sqrt(std::max(0.0, E_out * E_out - mD * mD)); - InteractionRecord r; - r.signature = sig; - r.primary_mass = mD; - r.primary_momentum = {E_D, 0, 0, std::sqrt(E_D * E_D - mD * mD)}; - r.target_mass = Constants::protonMass; - r.secondary_momenta = {{E_out, 0, 0, p_out}, {E_D - E_out, 0, 0, E_D - E_out}}; - r.secondary_masses = {mD, 0.0}; - // FinalStateProbability = DifferentialCrossSection / TotalCrossSection, - // i.e. the normalized truncated Gaussian density in z. - return xs.FinalStateProbability(r); - }; - - // (a) The density is non-negative everywhere and the empirical pdf closes - // bin-by-bin within ~4 Poisson sigma. - for (int b = 0; b < NB; ++b) { - double zc = zlo + (b + 0.5) * bw; - double pred = fsp_at_z(zc); - EXPECT_GE(pred, 0.0); - if (counts[b] < 50) continue; - double emp = (double)counts[b] / (N * bw); - double sigma = std::sqrt((double)counts[b]) / (N * bw); - EXPECT_NEAR(emp, pred, 4.0 * sigma + 0.02 * pred); - } + auto fsp_at_z = [&](double z) -> double { + double E_out = E_D * (1.0 - z); + double p_out = std::sqrt(std::max(0.0, E_out * E_out - mD * mD)); + InteractionRecord r; + r.signature = sig; + r.primary_mass = mD; + r.primary_momentum = {E_D, 0, 0, p_D}; + r.target_mass = Constants::protonMass; + r.secondary_momenta = {{E_out, 0, 0, p_out}, {E_D - E_out, 0, 0, E_D - E_out}}; + r.secondary_masses = {mD, 0.0}; + return xs.FinalStateProbability(r); + }; + + // (a) bin-by-bin closure within ~4 Poisson sigma. + for (int b = 0; b < NB; ++b) { + double zc = zlo + (b + 0.5) * bw; + double pred = fsp_at_z(zc); + EXPECT_GE(pred, 0.0); + if (counts[b] < 50) continue; + double emp = (double)counts[b] / (N * bw); + double sg = std::sqrt((double)counts[b]) / (N * bw); + EXPECT_NEAR(emp, pred, 4.0 * sg + 0.02 * pred) << "E_D=" << E_D << " z=" << zc; + } + + // (b) the density integrates to 1 over the realized support [zlo, zhi]. + int M = 4000; + double integral = 0.0, dz = (zhi - zlo) / M; + for (int i = 0; i <= M; ++i) { + double z = zlo + i * dz; + double w = (i == 0 || i == M) ? 0.5 : 1.0; + integral += w * fsp_at_z(z) * dz; + } + EXPECT_NEAR(integral, 1.0, 2e-2) << "E_D=" << E_D; - // (b) The density integrates to 1 over [z_min_, z_max_] (trapezoid on a - // fine grid) -- it is a properly normalized pdf in z. - int M = 2000; - double integral = 0.0; - double dz = (zhi - zlo) / M; - for (int i = 0; i <= M; ++i) { - double z = zlo + i * dz; - double w = (i == 0 || i == M) ? 0.5 : 1.0; - integral += w * fsp_at_z(z) * dz; + // (c) the density vanishes ABOVE the kinematic cut -- the fixed-interval + // bug was nonzero density where the sampler produces nothing. + double z_above = zhi + 0.4 * (1.0 - zhi); + if (z_above < 1.0 - 1e-9) { + EXPECT_EQ(fsp_at_z(z_above), 0.0) << "E_D=" << E_D << " z_above=" << z_above; + } } - EXPECT_NEAR(integral, 1.0, 2e-2); } int main(int argc, char** argv) { diff --git a/projects/interactions/public/SIREN/interactions/DMesonELoss.h b/projects/interactions/public/SIREN/interactions/DMesonELoss.h index 2899fe89d..483aaefbb 100644 --- a/projects/interactions/public/SIREN/interactions/DMesonELoss.h +++ b/projects/interactions/public/SIREN/interactions/DMesonELoss.h @@ -40,12 +40,16 @@ friend cereal::access; std::set primary_types_ = {siren::dataclasses::Particle::ParticleType::D0, siren::dataclasses::Particle::ParticleType::DPlus, siren::dataclasses::Particle::ParticleType::DsPlus, siren::dataclasses::Particle::ParticleType::D0Bar, siren::dataclasses::Particle::ParticleType::DMinus, siren::dataclasses::Particle::ParticleType::DsMinus}; std::set target_types_ = {siren::dataclasses::Particle::ParticleType::PPlus}; - // Truncation bounds for the inelasticity z. Single source of truth shared by - // SampleFinalState (rejection) and DifferentialCrossSection/FinalStateProbability - // (normalization), so the sampler support and the density support match exactly - // (closure). The Gaussian in z is normalized over [z_min_, z_max_]. + // Truncation bounds for the inelasticity z, shared by SampleFinalState + // (rejection) and DifferentialCrossSection/FinalStateProbability (normalization). + // The ACTUAL upper limit is the energy-dependent kinematic cut z <= 1 - mD/E + // (final_energy >= mD); both the sampler and the density apply it, and the + // Gaussian is normalized over [z_min_, min(z_max_, 1 - mD/E)] so the supports + // match exactly at every energy (closure). z_max_ = 1 lets kinematics set the + // top; z_min_ is a small floor keeping z > 0 (no energy gain) and away from the + // z -> 0 null-recoil degeneracy. static constexpr double z_min_ = 0.001; - static constexpr double z_max_ = 0.999; + static constexpr double z_max_ = 1.0; public: DMesonELoss(); From 530a75cdabd831e8162752e8b937394ae5c95a29 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 18:02:29 -0700 Subject: [PATCH 78/93] PythiaDISCrossSection: generalize outgoing-lepton ID, recoverable failure, guards Match the outgoing lepton by the signature's PDG instead of a hardcoded abs(pid) == 13, so nu_e and nu_tau CC produce events (NC charm stays unsupported). Throw InjectionFailure rather than std::runtime_error when charm generation fails so the Injector drops the event, and guard the getIndices no-lepton sentinel against an out-of-bounds read. --- .../private/PythiaDISCrossSection.cxx | 48 +++++++++++++------ .../test/PythiaDISCharmClosure_TEST.cxx | 11 +++-- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/projects/interactions/private/PythiaDISCrossSection.cxx b/projects/interactions/private/PythiaDISCrossSection.cxx index 094e51666..fbf37b59b 100644 --- a/projects/interactions/private/PythiaDISCrossSection.cxx +++ b/projects/interactions/private/PythiaDISCrossSection.cxx @@ -10,6 +10,10 @@ #include #include #include +#include +#include +#include +#include #include #include @@ -23,6 +27,7 @@ #include "SIREN/dataclasses/Particle.h" #include "SIREN/utilities/Random.h" #include "SIREN/utilities/Constants.h" +#include "SIREN/utilities/Errors.h" namespace siren { namespace interactions { @@ -327,7 +332,13 @@ double PythiaDISCrossSection::DifferentialCrossSection(dataclasses::InteractionR double primary_energy = interaction.primary_momentum[0]; std::map secondaries = getIndices(interaction.signature); - unsigned int lepton_index = secondaries["lepton"]; + int lepton_index_i = secondaries["lepton"]; + // Guard against a malformed/empty signature: getIndices returns -1 when no + // lepton is found, and using that as an unsigned index reads out of bounds. + if(lepton_index_i < 0 || static_cast(lepton_index_i) >= interaction.secondary_momenta.size()) { + return 0.0; + } + unsigned int lepton_index = static_cast(lepton_index_i); std::array const & mom3 = interaction.secondary_momenta[lepton_index]; rk::P4 p3(geom3::Vector3(mom3[1], mom3[2], mom3[3]), interaction.secondary_masses[lepton_index]); @@ -544,6 +555,12 @@ void PythiaDISCrossSection::SampleFinalState(dataclasses::CrossSectionDistributi // Bridge the SIREN RNG for this event siren_rndm_->rng_ = random; + // The outgoing primary lepton is fixed by the signature: the charged lepton + // for CC (e/mu/tau, signed), or the same-flavor neutrino for NC. Match Pythia's + // final state against that exact PDG instead of hardcoding the muon, so NC and + // nu_e / nu_tau work, not only nu_mu CC. + int expected_lepton_pdg = static_cast(record.signature.secondary_types[lepton_index]); + // Generate a Pythia event int max_attempts = 100; bool found_charm = false; @@ -552,8 +569,8 @@ void PythiaDISCrossSection::SampleFinalState(dataclasses::CrossSectionDistributi continue; } - // Find the muon and charmed hadron in the final state - int i_muon = -1; + // Find the outgoing lepton and charmed hadron in the final state + int i_lep = -1; int i_charm = -1; Pythia8::Vec4 p_remnant(0., 0., 0., 0.); @@ -561,11 +578,11 @@ void PythiaDISCrossSection::SampleFinalState(dataclasses::CrossSectionDistributi if (!pythia_->event[i].isFinal()) continue; int pid = pythia_->event[i].id(); - int abs_pid = std::abs(pid); - if (abs_pid == 13 && i_muon < 0) { - // Primary muon (first one found) - i_muon = i; + if (pid == expected_lepton_pdg && i_lep < 0) { + // Primary outgoing lepton matching the signature (CC charged + // lepton or NC neutrino, with the correct charge). + i_lep = i; } else if (IsCharmedHadron(pid) && i_charm < 0) { // First charmed hadron i_charm = i; @@ -575,7 +592,7 @@ void PythiaDISCrossSection::SampleFinalState(dataclasses::CrossSectionDistributi } } - if (i_muon >= 0 && i_charm >= 0) { + if (i_lep >= 0 && i_charm >= 0) { // Trust Pythia: accept whatever charm meson it produced and // overwrite the signature's pre-chosen (uniform) meson slot with // Pythia's actual PID. Natural Lund-string fragmentation then @@ -587,12 +604,12 @@ void PythiaDISCrossSection::SampleFinalState(dataclasses::CrossSectionDistributi found_charm = true; // Extract 4-momenta from Pythia (in Pythia's frame: nu along +z) - Pythia8::Vec4 p_mu_pythia = pythia_->event[i_muon].p(); + Pythia8::Vec4 p_lep_pythia = pythia_->event[i_lep].p(); Pythia8::Vec4 p_D_pythia = pythia_->event[i_charm].p(); // Get Pythia's DIS kinematics for weighting double pythia_x = pythia_->info.x2(); // proton PDF x (beam B) - double pythia_y = 1.0 - p_mu_pythia.e() / E_nu; + double pythia_y = 1.0 - p_lep_pythia.e() / E_nu; // Store in interaction parameters for weighting record.interaction_parameters.clear(); @@ -612,7 +629,7 @@ void PythiaDISCrossSection::SampleFinalState(dataclasses::CrossSectionDistributi return rk::P4(mom, mass); }; - double muon_mass = pythia_->event[i_muon].m(); + double lepton_mass = pythia_->event[i_lep].m(); double D_mass = pythia_->event[i_charm].m(); double remnant_mass = std::sqrt(std::max(0.0, p_remnant.e() * p_remnant.e() - @@ -620,7 +637,7 @@ void PythiaDISCrossSection::SampleFinalState(dataclasses::CrossSectionDistributi p_remnant.py() * p_remnant.py() - p_remnant.pz() * p_remnant.pz())); - rk::P4 p3 = pythia_to_siren(p_mu_pythia, muon_mass); + rk::P4 p3 = pythia_to_siren(p_lep_pythia, lepton_mass); rk::P4 p_D = pythia_to_siren(p_D_pythia, D_mass); // Remnant @@ -635,7 +652,7 @@ void PythiaDISCrossSection::SampleFinalState(dataclasses::CrossSectionDistributi siren::dataclasses::SecondaryParticleRecord & meson = secondaries[meson_index]; lepton.SetFourMomentum({p3.e(), p3.px(), p3.py(), p3.pz()}); - lepton.SetMass(muon_mass); + lepton.SetMass(lepton_mass); lepton.SetHelicity(record.primary_helicity); hadron.SetFourMomentum({p_rem.e(), p_rem.px(), p_rem.py(), p_rem.pz()}); @@ -651,7 +668,10 @@ void PythiaDISCrossSection::SampleFinalState(dataclasses::CrossSectionDistributi } if (!found_charm) { - throw std::runtime_error("PythiaDISCrossSection::SampleFinalState: Failed to generate charm event after " + std::to_string(max_attempts) + " attempts"); + // Recoverable per-event failure: throw InjectionFailure so Injector:: + // GenerateEvent catches it and drops/retries the event instead of a plain + // std::runtime_error, which would abort the entire generation run. + throw siren::utilities::InjectionFailure("PythiaDISCrossSection::SampleFinalState: Failed to generate charm event after " + std::to_string(max_attempts) + " attempts"); } } diff --git a/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx b/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx index a46ea38fa..65b0846a2 100644 --- a/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx +++ b/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx @@ -43,9 +43,12 @@ using namespace siren::dataclasses; namespace { double expected_ff(ParticleType d) { - if(d==ParticleType::D0 || d==ParticleType::D0Bar) return 0.6; - if(d==ParticleType::DPlus || d==ParticleType::DMinus) return 0.23; - if(d==ParticleType::DsPlus || d==ParticleType::DsMinus) return 0.15; + // Renormalized to sum to 1.0 over the implemented D species (the unmodeled + // Lambda_c fraction is folded in by dividing each raw value by 0.98). Mirrors + // PythiaDISCrossSection::FragmentationFraction. + if(d==ParticleType::D0 || d==ParticleType::D0Bar) return 0.6 / 0.98; + if(d==ParticleType::DPlus || d==ParticleType::DMinus) return 0.23 / 0.98; + if(d==ParticleType::DsPlus || d==ParticleType::DsMinus) return 0.15 / 0.98; return 0.0; } } // namespace @@ -66,7 +69,7 @@ TEST(PythiaDISCharmClosure, FragmentationPartitionAndClosure) { /*minimum_Q2=*/1.0, primaries, targets, /*pythia_data_path=*/"", /*pdf_set=*/"LHAPDF6:CT18NLO", /*units=*/"cm"); - const double ff_sum = 0.6 + 0.23 + 0.15; + const double ff_sum = (0.6 + 0.23 + 0.15) / 0.98; // renormalized -> 1.0 for(double E : {30.0, 100.0, 300.0}) { double sigma_incl = xs.TotalCrossSection(ParticleType::NuMu, E); From c9335c1f54708b2f7fc650718620a2ba3c09f7f3 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 19:05:05 -0700 Subject: [PATCH 79/93] PythiaDISCrossSection: optional differential spline, constant FSP fallback, coordinate fix The differential spline is now optional; the total spline stays mandatory. With no differential spline, FinalStateProbability returns 1.0, since the intractable Pythia density cancels between the injection and physical sides in the unbiased configuration. With one, it returns dsigma/sigma at the muon-reconstructed Bjorken x = Q2/(2 M E y) that SampleFinalState now stores, matching the coordinate DifferentialCrossSection reconstructs. --- .../private/PythiaDISCrossSection.cxx | 105 +++++++++++------- .../interactions/PythiaDISCrossSection.h | 37 +++--- 2 files changed, 89 insertions(+), 53 deletions(-) diff --git a/projects/interactions/private/PythiaDISCrossSection.cxx b/projects/interactions/private/PythiaDISCrossSection.cxx index fbf37b59b..6f7a2932c 100644 --- a/projects/interactions/private/PythiaDISCrossSection.cxx +++ b/projects/interactions/private/PythiaDISCrossSection.cxx @@ -32,22 +32,6 @@ namespace siren { namespace interactions { -namespace { -bool kinematicallyAllowed(double x, double y, double E, double M, double m) { - if(x > 1) return false; - if(x < ((m * m) / (2 * M * (E - m)))) return false; - if (x < 1e-6 || y < 1e-6) return false; - double d = 2 * (1 + (M * x) / (2 * E)); - double ad = 1 - m * m * ((1 / (2 * M * E * x)) + (1 / (2 * E * E))); - double term = 1 - ((m * m) / (2 * M * E * x)); - double bd = sqrt(term * term - ((m * m) / (E * E))); - double s = 2 * M * E; - double Q2 = s * x * y; - double Mc = siren::utilities::Constants::D0Mass; - return ((ad - bd) <= d * y and d * y <= (ad + bd)) && (Q2 * (1 - x) / x + pow(M, 2) >= pow(M + Mc, 2)); -} -} // anonymous namespace - // --- SIRENRndm: bridges SIREN RNG into Pythia --- class SIRENRndm : public Pythia8::RndmEngine { @@ -125,19 +109,32 @@ void PythiaDISCrossSection::SetUnits(std::string units) { } void PythiaDISCrossSection::LoadFromFile(std::string dd_crossSectionFile, std::string total_crossSectionFile) { - differential_cross_section_ = photospline::splinetable<>(dd_crossSectionFile.c_str()); - if(differential_cross_section_.get_ndim() != 3 && differential_cross_section_.get_ndim() != 2) - throw std::runtime_error("cross section spline has " + std::to_string(differential_cross_section_.get_ndim()) - + " dimensions, should have either 3 or 2"); + // Total spline is mandatory (drives interaction depth / position / survival). total_cross_section_ = photospline::splinetable<>(total_crossSectionFile.c_str()); if(total_cross_section_.get_ndim() != 1) throw std::runtime_error("Total cross section spline has " + std::to_string(total_cross_section_.get_ndim()) + " dimensions, should have 1"); + // Differential spline is OPTIONAL: an empty filename means "no differential", + // in which case FinalStateProbability returns a constant (unbiased-only). + if(dd_crossSectionFile.empty()) { + has_differential_ = false; + } else { + differential_cross_section_ = photospline::splinetable<>(dd_crossSectionFile.c_str()); + if(differential_cross_section_.get_ndim() != 3 && differential_cross_section_.get_ndim() != 2) + throw std::runtime_error("cross section spline has " + std::to_string(differential_cross_section_.get_ndim()) + + " dimensions, should have either 3 or 2"); + has_differential_ = true; + } } void PythiaDISCrossSection::LoadFromMemory(std::vector & differential_data, std::vector & total_data) { - differential_cross_section_.read_fits_mem(differential_data.data(), differential_data.size()); total_cross_section_.read_fits_mem(total_data.data(), total_data.size()); + if(!differential_data.empty()) { + differential_cross_section_.read_fits_mem(differential_data.data(), differential_data.size()); + has_differential_ = true; + } else { + has_differential_ = false; + } } void PythiaDISCrossSection::ReadParamsFromSplineTable() { @@ -347,18 +344,30 @@ double PythiaDISCrossSection::DifferentialCrossSection(dataclasses::InteractionR double Q2 = -q.dot(q); double lepton_mass = GetLeptonMass(interaction.signature.secondary_types[lepton_index]); double y = 1.0 - p2.dot(p3) / p2.dot(p1); - double x = Q2 / (2.0 * p2.dot(q)); + double p2q = p2.dot(q); + double x = (p2q != 0.0) ? Q2 / (2.0 * p2q) : -1.0; // muon-reconstructed Bjorken x + // Use the reconstructed (x,y) when they fall inside the spline domain; the + // spline's support is the validity region (no separate analytic charm-DIS + // kinematic gate, which assumes a model the Pythia-derived spline does not). double log_energy = log10(primary_energy); - std::array coordinates{{log_energy, log10(x), log10(y)}}; std::array centers; - - if (Q2 < minimum_Q2_ || !kinematicallyAllowed(x, y, primary_energy, target_mass_, lepton_mass) - || !differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { - // Fall back to saved parameters - x = interaction.interaction_parameters.at("bjorken_x"); - y = interaction.interaction_parameters.at("bjorken_y"); - Q2 = 2. * primary_energy * p2.e() * x * y; + bool ok = (x > 0.0 && x < 1.0 && y > 0.0 && y < 1.0 && Q2 >= minimum_Q2_); + if(ok) { + std::array coordinates{{log_energy, log10(x), log10(y)}}; + ok = differential_cross_section_.searchcenters(coordinates.data(), centers.data()); + } + if(!ok) { + // Fall back to the stored (x,y) -- the SAME muon-reconstructed Bjorken + // variables SampleFinalState records. Non-throwing: if absent (e.g. a + // fake record built for a rate query), the differential is undefined -> 0. + auto itx = interaction.interaction_parameters.find("bjorken_x"); + auto ity = interaction.interaction_parameters.find("bjorken_y"); + if(itx == interaction.interaction_parameters.end() || ity == interaction.interaction_parameters.end()) + return 0.0; + x = itx->second; + y = ity->second; + Q2 = 2.0 * primary_energy * p2.e() * x * y; } return DifferentialCrossSection(primary_energy, x, y, lepton_mass, Q2); } @@ -372,7 +381,7 @@ double PythiaDISCrossSection::DifferentialCrossSection(double energy, double x, if(y <= 0 || y >= 1) return 0.0; if(std::isnan(Q2)) Q2 = 2.0 * energy * target_mass_ * x * y; if(Q2 < minimum_Q2_) return 0; - if(!kinematicallyAllowed(x, y, energy, target_mass_, secondary_lepton_mass)) return 0; + (void)secondary_lepton_mass; // analytic charm-DIS gate removed (see above) std::array coordinates{{log_energy, log10(x), log10(y)}}; std::array centers; @@ -406,12 +415,18 @@ double PythiaDISCrossSection::FragmentationFraction(siren::dataclasses::Particle } double PythiaDISCrossSection::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { - // Return dsigma/sigma. The fragmentation fraction is now applied inside - // TotalCrossSection (per signature), so txs below already carries it; it - // cancels against the per-signature TotalCrossSection weight in - // CrossSectionProbability, leaving the correct kinematic density. We do NOT - // multiply by a fragfrac table here -- SampleFinalState trusts Pythia's - // natural Lund-string fragmentation for the actual D-type distribution. + // The final state is sampled by Pythia, whose per-event density is not + // analytically available. With NO differential spline we return a constant: + // in the standard unbiased configuration the same cross-section object + // supplies both the injection and physical densities, so this factor cancels + // in the weight ratio and only TotalCrossSection (interaction depth / + // position / survival) matters. Biasing the final-state kinematics is not + // supported in this mode. + if(!has_differential_) return 1.0; + + // A Pythia-derived differential spline was supplied: report the true density + // dsigma/sigma so weights remain correct under reweighting. The fragmentation + // fraction in TotalCrossSection cancels per-signature in CrossSectionProbability. double dxs = DifferentialCrossSection(interaction); double txs = TotalCrossSection(interaction); if (!std::isfinite(dxs) || !std::isfinite(txs) || dxs <= 0 || txs <= 0) return 0.0; @@ -607,9 +622,19 @@ void PythiaDISCrossSection::SampleFinalState(dataclasses::CrossSectionDistributi Pythia8::Vec4 p_lep_pythia = pythia_->event[i_lep].p(); Pythia8::Vec4 p_D_pythia = pythia_->event[i_charm].p(); - // Get Pythia's DIS kinematics for weighting - double pythia_x = pythia_->info.x2(); // proton PDF x (beam B) - double pythia_y = 1.0 - p_lep_pythia.e() / E_nu; + // DIS kinematics for weighting, reconstructed from the outgoing lepton + // exactly as DifferentialCrossSection does (muon-reconstructed Bjorken + // x = Q2/(2 M E y)), so an optional differential spline fit in this + // convention closes against the sampler. NOT info.x2() (the parton + // momentum fraction), which is a different variable. + double E_lep = p_lep_pythia.e(); + double pythia_y = 1.0 - E_lep / E_nu; + Pythia8::Vec4 p_nu_pythia(0.0, 0.0, E_nu, E_nu); // (px,py,pz,e), nu along +z + Pythia8::Vec4 q4 = p_nu_pythia - p_lep_pythia; + double Q2_lep = -q4.m2Calc(); + double pythia_x = (pythia_y > 0.0) + ? Q2_lep / (2.0 * target_mass_ * E_nu * pythia_y) + : 0.0; // Store in interaction parameters for weighting record.interaction_parameters.clear(); diff --git a/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h b/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h index 6ee70d22f..71b20fcf8 100644 --- a/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h +++ b/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h @@ -44,9 +44,14 @@ class SIRENRndm; class PythiaDISCrossSection : public CrossSection { friend cereal::access; private: - // Splines for total/differential cross section (SIREN weighting) + // Splines for total/differential cross section (SIREN weighting). + // The total spline is always required (drives interaction depth / position / + // survival). The differential spline is OPTIONAL: when absent, + // FinalStateProbability returns a constant that cancels in the unbiased + // weight (final state comes from Pythia, injection == physical). photospline::splinetable<> differential_cross_section_; photospline::splinetable<> total_cross_section_; + bool has_differential_ = false; // Pythia instance (mutable because SampleFinalState is const) mutable std::unique_ptr pythia_; @@ -152,19 +157,22 @@ friend cereal::access; void save(Archive & archive, std::uint32_t const version) const { if(version == 0) { splinetable_buffer buf; - buf.size = 0; - auto result_obj = differential_cross_section_.write_fits_mem(); - buf.data = result_obj.first; - buf.size = result_obj.second; - - std::vector diff_blob; - diff_blob.resize(buf.size); - std::copy((char*)buf.data, (char*)buf.data + buf.size, &diff_blob[0]); - - archive(::cereal::make_nvp("DifferentialCrossSectionSpline", diff_blob)); + archive(::cereal::make_nvp("HasDifferential", has_differential_)); + + // The differential spline is optional; only serialize it when present. + if(has_differential_) { + buf.size = 0; + auto diff_obj = differential_cross_section_.write_fits_mem(); + buf.data = diff_obj.first; + buf.size = diff_obj.second; + std::vector diff_blob; + diff_blob.resize(buf.size); + std::copy((char*)buf.data, (char*)buf.data + buf.size, &diff_blob[0]); + archive(::cereal::make_nvp("DifferentialCrossSectionSpline", diff_blob)); + } buf.size = 0; - result_obj = total_cross_section_.write_fits_mem(); + auto result_obj = total_cross_section_.write_fits_mem(); buf.data = result_obj.first; buf.size = result_obj.second; @@ -191,7 +199,10 @@ friend cereal::access; if(version == 0) { std::vector differential_data; std::vector total_data; - archive(::cereal::make_nvp("DifferentialCrossSectionSpline", differential_data)); + archive(::cereal::make_nvp("HasDifferential", has_differential_)); + if(has_differential_) { + archive(::cereal::make_nvp("DifferentialCrossSectionSpline", differential_data)); + } archive(::cereal::make_nvp("TotalCrossSectionSpline", total_data)); archive(::cereal::make_nvp("PrimaryTypes", primary_types_)); archive(::cereal::make_nvp("TargetTypes", target_types_)); From a97d23d332aebc7c566d1d7b4c583c38b5a1d0c3 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 19:10:19 -0700 Subject: [PATCH 80/93] PythiaDISCrossSection: add Pythia spline generators (total + optional differential) GeneratePythiaCharmSamples runs Pythia once per energy with the same configuration as SampleFinalState (shared through ApplyPythiaCharmConfig), returning per-energy sigma and flat muon-reconstructed (E, Bjorken x, y). python/pythia_charm_splines.py fits these with photospline into the mandatory 1D sigma(E) and an optional 3D d2sigma/dxdy. Requires SIREN_WITH_PYTHIA8=ON. --- .../private/PythiaDISCrossSection.cxx | 123 ++++++++++++------ .../pybindings/PythiaDISCrossSection.h | 18 ++- .../interactions/PythiaDISCrossSection.h | 12 ++ python/pythia_charm_splines.py | 108 +++++++++++++++ 4 files changed, 218 insertions(+), 43 deletions(-) create mode 100644 python/pythia_charm_splines.py diff --git a/projects/interactions/private/PythiaDISCrossSection.cxx b/projects/interactions/private/PythiaDISCrossSection.cxx index 6f7a2932c..e1b8e6d1a 100644 --- a/projects/interactions/private/PythiaDISCrossSection.cxx +++ b/projects/interactions/private/PythiaDISCrossSection.cxx @@ -40,6 +40,40 @@ class SIRENRndm : public Pythia8::RndmEngine { double flat() override { return rng_->Uniform(0.0, 1.0); } }; +namespace { +// Charm-DIS Pythia configuration shared by SampleFinalState (InitializePythia) +// and the spline generator, so generated total/differential splines correspond +// to exactly the events SampleFinalState produces. Route both through here to +// keep them in lockstep. +void ApplyPythiaCharmConfig(Pythia8::Pythia & pythia, int interaction_type, + int beam_id, int target_pdg, double E_nu, + std::string const & pdf_set, double minimum_Q2) { + if (interaction_type == 1) pythia.readString("WeakBosonExchange:ff2ff(t:W) = on"); + else if (interaction_type == 2) pythia.readString("WeakBosonExchange:ff2ff(t:gmZ) = on"); + pythia.readString("Beams:frameType = 2"); + pythia.readString("Beams:idA = " + std::to_string(beam_id)); + pythia.readString("Beams:idB = " + std::to_string(target_pdg)); + pythia.readString("Beams:eA = " + std::to_string(E_nu)); + pythia.readString("Beams:eB = 0."); + if (interaction_type == 1) { + // Charm-only CC: zero non-charm CKM so every event produces charm. + pythia.settings.forceParm("StandardModel:Vud", 0.0); + pythia.settings.forceParm("StandardModel:Vus", 0.0); + pythia.settings.forceParm("StandardModel:Vub", 0.0); + pythia.settings.forceParm("StandardModel:Vcb", 0.0); + } + pythia.readString("PDF:pSet = " + pdf_set); + pythia.readString("PDF:useHard = on"); + pythia.readString("PDF:pHardSet = " + pdf_set); + pythia.readString("HadronLevel:Hadronize = on"); + pythia.readString("HadronLevel:Decay = off"); + pythia.readString("PartonLevel:MPI = off"); + pythia.readString("PhaseSpace:mHatMin = 0.0"); + pythia.readString("PhaseSpace:Q2Min = " + std::to_string(minimum_Q2)); + pythia.readString("Print:quiet = on"); +} +} // anonymous namespace + // --- Constructors --- PythiaDISCrossSection::PythiaDISCrossSection() {} @@ -486,48 +520,7 @@ void PythiaDISCrossSection::InitializePythia(double E_nu, int target_pdg) const break; // use the first primary type } - // Process selection - if (interaction_type_ == 1) { - pythia_->readString("WeakBosonExchange:ff2ff(t:W) = on"); - } else if (interaction_type_ == 2) { - pythia_->readString("WeakBosonExchange:ff2ff(t:gmZ) = on"); - } - - // Beam setup: fixed target - pythia_->readString("Beams:frameType = 2"); - pythia_->readString("Beams:idA = " + std::to_string(beam_id)); - pythia_->readString("Beams:idB = " + std::to_string(target_pdg)); - pythia_->readString("Beams:eA = " + std::to_string(E_nu)); - pythia_->readString("Beams:eB = 0."); - - // Force charm-only for CC: zero out non-charm CKM elements - // Must use forceParm to bypass Pythia's range checks - if (interaction_type_ == 1) { - pythia_->settings.forceParm("StandardModel:Vud", 0.0); - pythia_->settings.forceParm("StandardModel:Vus", 0.0); - pythia_->settings.forceParm("StandardModel:Vub", 0.0); - pythia_->settings.forceParm("StandardModel:Vcb", 0.0); - // Keeps Vcd ~ 0.225 and Vcs ~ 0.973 -> every CC event produces charm - } - - // PDF - pythia_->readString("PDF:pSet = " + pdf_set_); - pythia_->readString("PDF:useHard = on"); - pythia_->readString("PDF:pHardSet = " + pdf_set_); - - // Hadronization: string fragmentation ON, D decays OFF - pythia_->readString("HadronLevel:Hadronize = on"); - pythia_->readString("HadronLevel:Decay = off"); - - // Disable MPI - pythia_->readString("PartonLevel:MPI = off"); - - // Phase space: remove mHatMin (default 4 GeV) to allow full kinematic range. - // Keep pTHatMinDiverge at default (1 GeV), giving effective Q2 > 1 GeV^2. - pythia_->readString("PhaseSpace:mHatMin = 0.0"); - pythia_->readString("PhaseSpace:Q2Min = " + std::to_string(minimum_Q2_)); - - pythia_->readString("Print:quiet = on"); + ApplyPythiaCharmConfig(*pythia_, interaction_type_, beam_id, target_pdg, E_nu, pdf_set_, minimum_Q2_); if (!pythia_->init()) { throw std::runtime_error("PythiaDISCrossSection: Pythia initialization failed"); @@ -542,6 +535,52 @@ void PythiaDISCrossSection::InitializePythia(double E_nu, int target_pdg) const pythia_initialized_ = true; } +void PythiaDISCrossSection::GeneratePythiaCharmSamples( + int interaction_type, int primary_pdg, int target_pdg, double target_mass, + std::string pdf_set, std::string pythia_data_path, double minimum_Q2, + std::vector const & energies, int n_events, + std::vector & out_sigma_mb, + std::vector & out_E, std::vector & out_x, std::vector & out_y) +{ + // Run Pythia once per energy (fast: ~ms/event after init) using the SAME + // configuration SampleFinalState uses, recording the muon-reconstructed + // Bjorken (x, y) per charm event and Pythia's generated cross section per + // energy. The python generate_*_spline helpers fit these into FITS splines. + out_sigma_mb.clear(); out_E.clear(); out_x.clear(); out_y.clear(); + if (!std::getenv("LHAPDF_DATA_PATH")) + throw std::runtime_error("GeneratePythiaCharmSamples: LHAPDF_DATA_PATH is not set"); + bool is_cc = (interaction_type == 1); + for (double E : energies) { + Pythia8::Pythia pythia(pythia_data_path, false); + ApplyPythiaCharmConfig(pythia, interaction_type, primary_pdg, target_pdg, E, pdf_set, minimum_Q2); + if (!pythia.init()) + throw std::runtime_error("GeneratePythiaCharmSamples: Pythia init failed"); + for (int i = 0; i < n_events; ++i) { + if (!pythia.next()) continue; + int i_lep = -1; + for (int j = 0; j < pythia.event.size(); ++j) { + if (!pythia.event[j].isFinal()) continue; + int ap = std::abs(pythia.event[j].id()); + bool is_lep = is_cc ? (ap == 11 || ap == 13 || ap == 15) + : (ap == 12 || ap == 14 || ap == 16); + if (is_lep) { i_lep = j; break; } + } + if (i_lep < 0) continue; + Pythia8::Vec4 pl = pythia.event[i_lep].p(); + double y = 1.0 - pl.e() / E; + if (y <= 0.0 || y >= 1.0) continue; + Pythia8::Vec4 pnu(0.0, 0.0, E, E); // (px,py,pz,e), nu along +z + Pythia8::Vec4 q4 = pnu - pl; + double Q2 = -q4.m2Calc(); + if (Q2 <= 0.0) continue; + double x = Q2 / (2.0 * target_mass * E * y); + if (x <= 0.0 || x >= 1.0) continue; + out_E.push_back(E); out_x.push_back(x); out_y.push_back(y); + } + out_sigma_mb.push_back(pythia.info.sigmaGen()); + } +} + void PythiaDISCrossSection::SampleFinalState(dataclasses::CrossSectionDistributionRecord & record, std::shared_ptr random) const { // Get indices for lepton, hadron, meson in the secondary list std::map secondary_indices = getIndices(record.signature); diff --git a/projects/interactions/private/pybindings/PythiaDISCrossSection.h b/projects/interactions/private/pybindings/PythiaDISCrossSection.h index f6ab7c47f..2d78fae47 100644 --- a/projects/interactions/private/pybindings/PythiaDISCrossSection.h +++ b/projects/interactions/private/pybindings/PythiaDISCrossSection.h @@ -68,7 +68,23 @@ void register_PythiaDISCrossSection(pybind11::module_ & m) { .def("FinalStateProbability",&PythiaDISCrossSection::FinalStateProbability) .def("GetMinimumQ2",&PythiaDISCrossSection::GetMinimumQ2) .def("GetTargetMass",&PythiaDISCrossSection::GetTargetMass) - .def("GetInteractionType",&PythiaDISCrossSection::GetInteractionType); + .def("GetInteractionType",&PythiaDISCrossSection::GetInteractionType) + .def_static("GeneratePythiaCharmSamples", + [](int interaction_type, int primary_pdg, int target_pdg, double target_mass, + std::string pdf_set, std::string pythia_data_path, double minimum_Q2, + std::vector energies, int n_events) { + std::vector sigma_mb, E, x, y; + PythiaDISCrossSection::GeneratePythiaCharmSamples( + interaction_type, primary_pdg, target_pdg, target_mass, + pdf_set, pythia_data_path, minimum_Q2, energies, n_events, + sigma_mb, E, x, y); + return pybind11::make_tuple(sigma_mb, E, x, y); + }, + arg("interaction_type"), arg("primary_pdg"), arg("target_pdg"), arg("target_mass"), + arg("pdf_set"), arg("pythia_data_path"), arg("minimum_Q2"), + arg("energies"), arg("n_events"), + "Run Pythia (init once per energy) and return (sigma_mb_per_E, E, x, y) raw samples " + "for building charm-DIS splines. See siren.interactions.pythia_charm_splines."); } #else diff --git a/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h b/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h index 71b20fcf8..36e5cf1a1 100644 --- a/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h +++ b/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h @@ -138,6 +138,18 @@ friend cereal::access; // Final state sampling via Pythia void SampleFinalState(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr random) const override; + // Generate raw Pythia charm-DIS samples for building total/differential + // splines (init once per energy, using the same Pythia config as + // SampleFinalState). out_sigma_mb is per energy (Pythia's generated cross + // section, mb); out_E/out_x/out_y are flat per-event muon-reconstructed + // (E, Bjorken x, y). Consumed by the python generate_*_spline helpers. + static void GeneratePythiaCharmSamples( + int interaction_type, int primary_pdg, int target_pdg, double target_mass, + std::string pdf_set, std::string pythia_data_path, double minimum_Q2, + std::vector const & energies, int n_events, + std::vector & out_sigma_mb, + std::vector & out_E, std::vector & out_x, std::vector & out_y); + // Signature methods std::vector GetPossibleTargets() const override; std::vector GetPossibleTargetsFromPrimary(siren::dataclasses::ParticleType primary_type) const override; diff --git a/python/pythia_charm_splines.py b/python/pythia_charm_splines.py new file mode 100644 index 000000000..1d1a9a8d5 --- /dev/null +++ b/python/pythia_charm_splines.py @@ -0,0 +1,108 @@ +"""Generate Pythia-derived charm-DIS splines for ``PythiaDISCrossSection``. + +``PythiaDISCrossSection`` samples the final state from Pythia and uses splines for +the cross-section *rate* (and, optionally, a differential density). These helpers +build those splines directly from Pythia so they correspond to exactly the events +the sampler produces: + + * :func:`generate_total_spline` -> 1D ``sigma(E)`` FITS (always required). + * :func:`generate_differential_spline` -> 3D ``d2sigma/dx dy`` FITS (optional; + supplying it gives ``FinalStateProbability`` a real density so weights stay + correct under reweighting; omitting it makes ``FinalStateProbability`` a + constant that cancels in the unbiased weight). + +Both run Pythia via ``PythiaDISCrossSection.GeneratePythiaCharmSamples`` and fit +with photospline. ``LHAPDF_DATA_PATH`` must be set in the environment, and SIREN +must be built with ``SIREN_WITH_PYTHIA8=ON``. + +The differential is fit in the muon-reconstructed Bjorken ``x`` (the same variable +``DifferentialCrossSection`` reconstructs), so it closes against the sampler. +""" +import numpy as np + + +def _knots(c, order=2): + c = np.asarray(c, float) + d = c[1] - c[0] + return np.concatenate([c[0] - d * np.arange(order, 0, -1), c, + c[-1] + d * np.arange(1, order + 1)]) + + +def _run_pythia(interaction_type, primary_pdg, target_pdg, target_mass, pdf_set, + pythia_data_path, minimum_Q2, energies, n_events): + import siren.interactions + sigma_mb, E, x, y = siren.interactions.PythiaDISCrossSection.GeneratePythiaCharmSamples( + int(interaction_type), int(primary_pdg), int(target_pdg), float(target_mass), + str(pdf_set), str(pythia_data_path), float(minimum_Q2), + [float(e) for e in energies], int(n_events)) + return np.asarray(sigma_mb), np.asarray(E), np.asarray(x), np.asarray(y) + + +def generate_total_spline(out_path, *, interaction_type, primary_pdg, target_pdg, + target_mass, pdf_set, pythia_data_path, energies, + minimum_Q2=1.0, n_events=20000, mb_to_cm2=1e-27): + """Fit and write the 1D total cross-section spline ``log10(sigma_cm2)`` vs ``log10(E)``.""" + import photospline + sigma_mb, _, _, _ = _run_pythia(interaction_type, primary_pdg, target_pdg, + target_mass, pdf_set, pythia_data_path, + minimum_Q2, energies, n_events) + logE = np.log10(np.asarray(energies, float)) + logsig = np.log10(sigma_mb * mb_to_cm2) + z, w = photospline.ndsparse.from_data(logsig, np.ones_like(logsig)) + spline = photospline.glam_fit(z, w, [logE], [_knots(logE)], [2], [1e-3], [2]) + spline.write(out_path) + return out_path + + +def generate_differential_spline(out_path, *, interaction_type, primary_pdg, target_pdg, + target_mass, pdf_set, pythia_data_path, energies, + minimum_Q2=1.0, n_events=50000, + logx_range=(-4.0, -0.1), logy_range=(-2.3, -0.004), + nbins=12, mb_to_cm2=1e-27): + """Fit and write the 3D differential spline ``log10(d2sigma/dx dy)`` vs ``(log10 E, log10 x, log10 y)``. + + The per-bin density is ``sigma(E) * dN/dx dy / N`` (log-space Jacobian folded + in) so that ``FinalStateProbability = dsigma/sigma`` reproduces the sampled + ``(x, y)`` density. + """ + import photospline + sigma_mb, E, x, y = _run_pythia(interaction_type, primary_pdg, target_pdg, + target_mass, pdf_set, pythia_data_path, + minimum_Q2, energies, n_events) + energies = np.asarray(energies, float) + logE_grid = np.log10(energies) + logx_edges = np.linspace(logx_range[0], logx_range[1], nbins + 1) + logy_edges = np.linspace(logy_range[0], logy_range[1], nbins + 1) + logx_c = 0.5 * (logx_edges[:-1] + logx_edges[1:]) + logy_c = 0.5 * (logy_edges[:-1] + logy_edges[1:]) + dlx = logx_edges[1] - logx_edges[0] + dly = logy_edges[1] - logy_edges[0] + ln10 = np.log(10.0) + nE, nx, ny = len(energies), len(logx_c), len(logy_c) + Z = np.full((nE, nx, ny), -99.0) + W = np.zeros((nE, nx, ny)) + for ie, Eval in enumerate(energies): + sigma = sigma_mb[ie] * mb_to_cm2 + m = (E == Eval) + Ntot = int(m.sum()) + if Ntot == 0: + continue + H, _, _ = np.histogram2d(np.log10(x[m]), np.log10(y[m]), + bins=[logx_edges, logy_edges]) + for ix in range(nx): + xc = 10.0 ** logx_c[ix] + for iy in range(ny): + c = H[ix, iy] + if c <= 0: + continue + yc = 10.0 ** logy_c[iy] + dxdy = (xc * ln10 * dlx) * (yc * ln10 * dly) + Z[ie, ix, iy] = np.log10(sigma * c / (Ntot * dxdy)) + W[ie, ix, iy] = c + z, w = photospline.ndsparse.from_data(Z, W) + spline = photospline.glam_fit( + z, w, [logE_grid, logx_c, logy_c], + [_knots(logE_grid), _knots(logx_c), _knots(logy_c)], + [2, 2, 2], [5e-2, 5e-2, 5e-2], [2, 2, 2]) + spline.write(out_path) + return out_path From 62f498bd6a64d289e40fdd302244073ce02cb8a3 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 19:12:16 -0700 Subject: [PATCH 81/93] PythiaDISCharmClosure_TEST: assert constant FinalStateProbability without a differential spline --- .../test/PythiaDISCharmClosure_TEST.cxx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx b/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx index 65b0846a2..8e664187a 100644 --- a/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx +++ b/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx @@ -113,6 +113,38 @@ TEST(PythiaDISCharmClosure, FragmentationPartitionAndClosure) { EXPECT_NEAR(gen, sum, sigma_incl * 1e-9) << "generation/physical interaction depth mismatch"; } } + +// The differential spline is optional. With only a total spline (empty +// differential filename), FinalStateProbability must return a constant 1.0 -- +// the Pythia final-state density is intractable but cancels in the unbiased +// weight, so only the total cross section matters. Needs only the total spline. +TEST(PythiaDISCharmClosure, ConstantFinalStateProbabilityWithoutDifferential) { + const char* tt = std::getenv("SIREN_PYTHIA_TEST_SIGMA"); + if(!tt) { + GTEST_SKIP() << "Set SIREN_PYTHIA_TEST_SIGMA to run."; + } + std::vector primaries = { ParticleType::NuMu }; + std::vector targets = { ParticleType::PPlus }; + PythiaDISCrossSection xs( + /*differential=*/std::string(""), std::string(tt), + /*interaction_type=*/1, /*target_mass=*/0.9382720813, + /*minimum_Q2=*/1.0, primaries, targets, + /*pythia_data_path=*/"", /*pdf_set=*/"LHAPDF6:CT18NLO", /*units=*/"cm"); + + // Total still works. + EXPECT_GT(xs.TotalCrossSection(ParticleType::NuMu, 100.0), 0.0); + + // FinalStateProbability is exactly 1.0 for any well-formed record, since the + // no-differential branch returns before touching kinematics. + auto sig = xs.GetPossibleSignaturesFromParents(ParticleType::NuMu, ParticleType::PPlus)[0]; + InteractionRecord rec; + rec.signature = sig; + rec.primary_momentum = {100.0, 0.0, 0.0, 100.0}; + rec.target_mass = 0.9382720813; + rec.secondary_momenta = {{40.0, 1.0, 0.0, 39.0}, {0.0, 0.0, 0.0, 0.0}, {1.86, 0.0, 0.0, 1.0}}; + rec.secondary_masses = {0.105, 0.0, 1.86}; + EXPECT_EQ(xs.FinalStateProbability(rec), 1.0); +} #endif // SIREN_HAS_PYTHIA8 int main(int argc, char** argv) { From 21baf78050245ea2edba05cc68e862846f57883a Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 21:03:59 -0700 Subject: [PATCH 82/93] PythiaDIS/CharmMesonDecay: reject misuse with actionable errors PythiaDISCrossSection rejects neutral current at construction, pre-checks that the LHAPDF set exists under LHAPDF_DATA_PATH, and reports the valid energy band on out-of-range. CharmMesonDecay and CharmMesonDecay3Body guard an empty signature, and CharmMesonDecay3Body throws on species other than D0/D+. --- .../interactions/private/CharmMesonDecay.cxx | 8 +++++ .../private/CharmMesonDecay3Body.cxx | 12 ++++++- .../private/PythiaDISCrossSection.cxx | 36 +++++++++++++++++-- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx index 46d496747..9a67c6142 100644 --- a/projects/interactions/private/CharmMesonDecay.cxx +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -843,6 +843,11 @@ double CharmMesonDecay::SampledQ2Normalization(double mD, double mK, double ml, double CharmMesonDecay::FinalStateProbability(dataclasses::InteractionRecord const & record) const { dataclasses::InteractionSignature signature = record.signature; + // Guard: finalize() does not copy the signature, so a finalized record can + // arrive with an empty signature/secondaries -> indexing below would be UB. + if (signature.secondary_types.empty()) { + throw std::runtime_error("CharmMesonDecay::FinalStateProbability: record has an empty signature. Set record.signature (and secondary masses/momenta) before calling; finalize() does not copy the signature."); + } // Fully hadronic catch-all: a single Hadrons daughter carries the full // primary 4-momentum, so the final state is deterministic and its density is 1. if (signature.secondary_types.size() == 1 && @@ -855,6 +860,9 @@ double CharmMesonDecay::FinalStateProbability(dataclasses::InteractionRecord con primary == siren::dataclasses::Particle::ParticleType::DsMinus); bool apply_va = !is_Ds; // Ds is pure phase space (no V-A matrix element) + if (record.secondary_masses.size() < 2 || record.secondary_momenta.empty()) { + throw std::runtime_error("CharmMesonDecay::FinalStateProbability: record secondaries are not populated (need >=2 masses and >=1 momentum)."); + } // Reconstruct q^2 = (p_D - p_hadron)^2 exactly as SampleFinalState defines it. double mD = record.primary_mass; double ml = record.secondary_masses[1]; diff --git a/projects/interactions/private/CharmMesonDecay3Body.cxx b/projects/interactions/private/CharmMesonDecay3Body.cxx index 4f6430b01..fe701916e 100644 --- a/projects/interactions/private/CharmMesonDecay3Body.cxx +++ b/projects/interactions/private/CharmMesonDecay3Body.cxx @@ -63,6 +63,8 @@ CharmMesonDecay3Body::CharmMesonDecay3Body(siren::dataclasses::Particle::Particl mD = particleMass(siren::dataclasses::Particle::ParticleType::D0); mK = particleMass(siren::dataclasses::Particle::ParticleType::KMinus); + } else { + throw std::runtime_error("CharmMesonDecay3Body: only D0 and D+ are implemented. Use CharmMesonDecay, which covers D0/D+/Ds and their anti-flavors."); } computeDiffGammaCDF(constants, mD, mK); @@ -235,7 +237,7 @@ std::vector CharmMesonDecay3Body::GetPossible signatures.push_back(hadron_signature); } else { - std::cout << "this D meson decay has not been implemented yet" << std::endl; + throw std::runtime_error("CharmMesonDecay3Body::GetPossibleSignaturesFromParent: only D0 and D+ are implemented. Use CharmMesonDecay for D0/D+/Ds and anti-flavors."); } return signatures; } @@ -649,6 +651,11 @@ double CharmMesonDecay3Body::SampledQ2Normalization(double mD, double mK, double double CharmMesonDecay3Body::FinalStateProbability(dataclasses::InteractionRecord const & record) const { dataclasses::InteractionSignature signature = record.signature; + // Guard: finalize() does not copy the signature, so a finalized record can + // arrive with an empty signature/secondaries -> indexing below would be UB. + if (signature.secondary_types.empty()) { + throw std::runtime_error("CharmMesonDecay3Body::FinalStateProbability: record has an empty signature. Set record.signature (and secondary masses/momenta) before calling; finalize() does not copy the signature."); + } // Fully hadronic catch-all: deterministic final state, density 1. if (signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::Hadrons) { return 1.0; @@ -657,6 +664,9 @@ double CharmMesonDecay3Body::FinalStateProbability(dataclasses::InteractionRecor siren::dataclasses::Particle::ParticleType primary = signature.primary_type; bool apply_va = true; // 3-body class always applies the V-A matrix element + if (record.secondary_masses.size() < 2 || record.secondary_momenta.empty()) { + throw std::runtime_error("CharmMesonDecay3Body::FinalStateProbability: record secondaries are not populated (need >=2 masses and >=1 momentum)."); + } double mD = record.primary_mass; double ml = record.secondary_masses[1]; double mK = record.secondary_masses[0]; // hadron mass actually sampled diff --git a/projects/interactions/private/PythiaDISCrossSection.cxx b/projects/interactions/private/PythiaDISCrossSection.cxx index e1b8e6d1a..cb321cedd 100644 --- a/projects/interactions/private/PythiaDISCrossSection.cxx +++ b/projects/interactions/private/PythiaDISCrossSection.cxx @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -248,6 +249,16 @@ std::map PythiaDISCrossSection::getIndices(siren::dataclasses: // --- Signatures --- void PythiaDISCrossSection::InitializeSignatures() { + // PythiaDISCrossSection forces charm only in charged-current (the non-charm + // CKM elements are zeroed in ApplyPythiaCharmConfig). Z exchange has no such + // handle, so NC charm cannot be forced and SampleFinalState would exhaust its + // attempt budget and throw. Reject NC at construction with an actionable + // pointer to the tool that does support NC charm (QuarkDISFromSpline). + if(interaction_type_ != 1) { + if(interaction_type_ == 2) + throw std::runtime_error("PythiaDISCrossSection supports only charged-current charm production (interaction_type=1). Neutral-current charm is not forced by Z exchange in Pythia, so per-event sampling would fail; use QuarkDISFromSpline for NC charm."); + throw std::runtime_error("PythiaDISCrossSection: unsupported interaction_type=" + std::to_string(interaction_type_) + " (expected 1 for charged current)."); + } signatures_.clear(); for(auto primary_type : primary_types_) { dataclasses::InteractionSignature signature; @@ -348,7 +359,11 @@ double PythiaDISCrossSection::TotalCrossSection(siren::dataclasses::ParticleType double log_energy = log10(primary_energy); if(log_energy < total_cross_section_.lower_extent(0) or log_energy > total_cross_section_.upper_extent(0)) { - throw std::runtime_error("Interaction energy out of cross section table range"); + throw std::runtime_error("PythiaDISCrossSection: primary energy " + + std::to_string(primary_energy) + " GeV is outside the total cross-section spline range [" + + std::to_string(std::pow(10.0, total_cross_section_.lower_extent(0))) + ", " + + std::to_string(std::pow(10.0, total_cross_section_.upper_extent(0))) + + "] GeV; regenerate the total spline (siren.pythia_charm_splines.generate_total_spline) to cover this energy."); } int center; total_cross_section_.searchcenters(&log_energy, ¢er); @@ -508,7 +523,24 @@ void PythiaDISCrossSection::InitializePythia(double E_nu, int target_pdg) const // The pdf_set_ is e.g. "LHAPDF6:HERAPDF20_NLO_EIG", and LHAPDF needs LHAPDF_DATA_PATH set const char* lhapdf_path = std::getenv("LHAPDF_DATA_PATH"); if (!lhapdf_path) { - throw std::runtime_error("LHAPDF_DATA_PATH is not set; set it to your LHAPDF data directory"); + throw std::runtime_error("LHAPDF_DATA_PATH is not set; set it to your LHAPDF data directory (the parent of the PDF set folders)."); + } + + // Pre-flight: verify the requested PDF set is actually installed under + // LHAPDF_DATA_PATH. A missing set otherwise surfaces only as Pythia's opaque + // "initialization failed" much later; name the set and where we looked. + { + std::string set_name = pdf_set_; + auto colon = set_name.find(':'); // strip an "LHAPDF6:" prefix + if (colon != std::string::npos) set_name = set_name.substr(colon + 1); + std::string set_dir = std::string(lhapdf_path) + "/" + set_name; + struct stat st; + if (stat(set_dir.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) { + throw std::runtime_error("PythiaDISCrossSection: LHAPDF PDF set '" + set_name + + "' not found under LHAPDF_DATA_PATH=" + std::string(lhapdf_path) + + " (looked for " + set_dir + "). Install it (e.g. 'lhapdf install " + set_name + + "') or construct with a pdf_set that is installed."); + } } pythia_ = std::make_unique(pythia_data_path_, false); From a335b96ccfa74c46fae24adc97702802c6074a94 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 21:03:59 -0700 Subject: [PATCH 83/93] Add charm-DIS physics validation tests C++ tests cover the per-species lab decay length L = beta*gamma*c*tau and its ordering, NC rejection, the fragmentation-fraction tripwire, and the partition/closure invariant up to 1 PeV. Env-gated Python tests cover charm-rate normalization, SampleFinalState reproducing Pythia's Bjorken x/y, D-meson energy fraction and collimation, differential-spline coverage, and an end-to-end inject -> weight closure. --- .../test/CharmMesonDecay3Body_TEST.cxx | 12 + .../private/test/CharmMesonDecay_TEST.cxx | 76 ++++ .../test/PythiaDISCharmClosure_TEST.cxx | 92 +++++ tests/python/test_pythia_charm_validation.py | 346 ++++++++++++++++++ 4 files changed, 526 insertions(+) create mode 100644 tests/python/test_pythia_charm_validation.py diff --git a/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx b/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx index fa176a7f7..ebd8d05fa 100644 --- a/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx +++ b/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx @@ -405,6 +405,18 @@ TEST(CharmMesonDecay3Body, VAWeightAngleAverageMatchesNumericReference) { } } +// CharmMesonDecay3Body implements only D0 and D+. Unsupported species (e.g. Ds) +// and empty-signature records must fail loudly rather than silently mis-decay or +// index out of bounds. (CharmMesonDecay covers D0/D+/Ds and anti-flavors.) +TEST(CharmMesonDecay3Body, UnsupportedSpeciesAndEmptySignatureThrow) { + EXPECT_THROW({ CharmMesonDecay3Body d(ParticleType::DsPlus); }, std::runtime_error); + CharmMesonDecay3Body dp(ParticleType::DPlus); + EXPECT_THROW(dp.GetPossibleSignaturesFromParent(ParticleType::DsPlus), std::runtime_error); + CharmMesonDecay3Body d0(ParticleType::D0); + InteractionRecord rec; // default: empty signature + EXPECT_THROW(d0.FinalStateProbability(rec), std::runtime_error); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx index ff4470d15..3ffc4200f 100644 --- a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx +++ b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx @@ -500,3 +500,79 @@ TEST(CharmMesonDecay, VAWeightAngleAverageMatchesNumericReference) { } } } + +// --- Test 6: lab decay length L = beta*gamma*c*tau (cascade separation) ------ +// +// The multi-cascade search reconstructs the separation L between the production +// cascade and the charm-decay cascade; the hard regime is below ~10 m. L is set +// by the D species proper lifetime and the lab boost, so it must be physically +// correct across the analysis energy band (TeV-PeV). Decay::TotalDecayLength +// returns beta*gamma*(1/Gamma)*hbarc, and because the modeled branching ratios +// sum to 1 the width recovers the PDG lifetime exactly. +namespace { +constexpr double tau_D0_s = 410.1e-15; // proper lifetimes hardcoded in +constexpr double tau_Dp_s = 1040.0e-15; // CharmMesonDecay (seconds). +constexpr double tau_Ds_s = 504.0e-15; +constexpr double c_m_per_s = 2.99792458e8; // speed of light [m/s] +// Note: Decay::TotalDecayLength returns METERS (SIREN base length unit; hbarc +// carries the cm->m conversion), consistent with the Taupede reco frame. + +InteractionRecord make_boosted_D(ParticleType d, double mD, double E_D) { + double p = std::sqrt(E_D * E_D - mD * mD); + InteractionRecord rec; + rec.signature.primary_type = d; + rec.signature.target_type = ParticleType::Decay; + rec.primary_mass = mD; + rec.primary_momentum = {E_D, 0.0, 0.0, p}; // boosted along +z + return rec; +} +} // namespace + +TEST(CharmMesonDecay, LabDecayLengthIsBetaGammaCTau) { + struct Case { ParticleType d; double mD; double tau; }; + std::vector cases = { + {ParticleType::D0, Constants::D0Mass, tau_D0_s}, + {ParticleType::DPlus, Constants::DPlusMass, tau_Dp_s}, + {ParticleType::DsPlus, Constants::DsPlusMass, tau_Ds_s}, + }; + for (auto const & cs : cases) { + CharmMesonDecay decay(cs.d); + double prev_L = -1.0, prev_E = -1.0; + for (double E_D : {1.0e4, 1.0e5, 1.0e6}) { // 10 TeV, 100 TeV, 1 PeV + InteractionRecord rec = make_boosted_D(cs.d, cs.mD, E_D); + double L_m = decay.TotalDecayLength(rec); // SIREN [m] + double p = std::sqrt(E_D * E_D - cs.mD * cs.mD); + double betagamma = p / cs.mD; + double expected_m = betagamma * c_m_per_s * cs.tau; // physics truth [m] + // 0.5% absorbs the ~0.01% rounding of SIREN's hbar/hbarc constants. + EXPECT_NEAR(L_m, expected_m, 5e-3 * expected_m) + << "species=" << static_cast(cs.d) << " E_D=" << E_D; + // beta*gamma is linear in E for E >> m: L(10x E) ~ 10x L. + if (prev_L > 0.0) + EXPECT_NEAR(L_m / prev_L, E_D / prev_E, 1e-3 * (E_D / prev_E)); + prev_L = L_m; prev_E = E_D; + } + } +} + +TEST(CharmMesonDecay, LabDecayLengthSpeciesOrdering) { + // At fixed boost energy the L ordering follows the lifetimes: D+ > Ds > D0. + double E_D = 1.0e5; // 100 TeV + CharmMesonDecay d0(ParticleType::D0), dp(ParticleType::DPlus), ds(ParticleType::DsPlus); + double L_D0 = d0.TotalDecayLength(make_boosted_D(ParticleType::D0, Constants::D0Mass, E_D)); + double L_Dp = dp.TotalDecayLength(make_boosted_D(ParticleType::DPlus, Constants::DPlusMass, E_D)); + double L_Ds = ds.TotalDecayLength(make_boosted_D(ParticleType::DsPlus, Constants::DsPlusMass, E_D)); + EXPECT_GT(L_Dp, L_Ds); + EXPECT_GT(L_Ds, L_D0); + // At 100 TeV a D0 travels a few meters -- squarely in the Taupede regime. + EXPECT_GT(L_D0, 1.0); // > 1 m + EXPECT_LT(L_D0, 50.0); // < 50 m +} + +TEST(CharmMesonDecay, FinalStateProbabilityThrowsOnEmptySignature) { + // finalize() does not copy the signature; FinalStateProbability must reject + // an empty-signature record loudly instead of indexing out of bounds (UB). + CharmMesonDecay decay(ParticleType::D0); + InteractionRecord rec; // default: empty signature, no secondaries + EXPECT_THROW(decay.FinalStateProbability(rec), std::runtime_error); +} diff --git a/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx b/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx index 8e664187a..fc967c32b 100644 --- a/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx +++ b/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx @@ -145,6 +145,98 @@ TEST(PythiaDISCharmClosure, ConstantFinalStateProbabilityWithoutDifferential) { rec.secondary_masses = {0.105, 0.0, 1.86}; EXPECT_EQ(xs.FinalStateProbability(rec), 1.0); } + +// PythiaDISCrossSection forces charm only in charged current. NC charm is not +// forced by Z exchange, so it must be rejected at construction with an +// actionable error rather than failing later inside SampleFinalState. +TEST(PythiaDISCharmClosure, NeutralCurrentRejectedAtConstruction) { + const char* tt = std::getenv("SIREN_PYTHIA_TEST_SIGMA"); + if(!tt) GTEST_SKIP() << "Set SIREN_PYTHIA_TEST_SIGMA to run."; + std::vector primaries = { ParticleType::NuMu }; + std::vector targets = { ParticleType::PPlus }; + EXPECT_THROW({ + PythiaDISCrossSection xs(std::string(""), std::string(tt), + /*interaction_type=*/2, 0.9382720813, 1.0, primaries, targets, + "", "LHAPDF6:CT18NLO", "cm"); + }, std::runtime_error); + // Positive control: CC still constructs cleanly. + EXPECT_NO_THROW({ + PythiaDISCrossSection xs(std::string(""), std::string(tt), + /*interaction_type=*/1, 0.9382720813, 1.0, primaries, targets, + "", "LHAPDF6:CT18NLO", "cm"); + }); +} + +// Species/fragmentation tripwire. The three modeled D fractions are renormalized +// (each /0.98) so the partitioned signatures recover the inclusive charm cross +// section; the unmodeled Lambda_c (~0.02 of the D0/D+/Ds total) is folded in. +// Pin the raw sum (0.98) and Lambda_c -> 0 so adding Lambda_c forces a test +// update rather than silently shifting the species mix. +TEST(PythiaDISCharmClosure, FragmentationFractionSpeciesTripwire) { + const char* tt = std::getenv("SIREN_PYTHIA_TEST_SIGMA"); + if(!tt) GTEST_SKIP() << "Set SIREN_PYTHIA_TEST_SIGMA to run."; + std::vector primaries = { ParticleType::NuMu }; + std::vector targets = { ParticleType::PPlus }; + PythiaDISCrossSection xs(std::string(""), std::string(tt), + 1, 0.9382720813, 1.0, primaries, targets, "", "LHAPDF6:CT18NLO", "cm"); + + EXPECT_NEAR(xs.FragmentationFraction(ParticleType::D0), 0.6 / 0.98, 1e-12); + EXPECT_NEAR(xs.FragmentationFraction(ParticleType::DPlus), 0.23 / 0.98, 1e-12); + EXPECT_NEAR(xs.FragmentationFraction(ParticleType::DsPlus), 0.15 / 0.98, 1e-12); + // Renormalized fractions over the modeled species recover the full inclusive sigma. + double sum = xs.FragmentationFraction(ParticleType::D0) + + xs.FragmentationFraction(ParticleType::DPlus) + + xs.FragmentationFraction(ParticleType::DsPlus); + EXPECT_NEAR(sum, 1.0, 1e-12); + // Raw fractions sum to 0.98; the 0.02 gap is the folded unmodeled-baryon fraction. + EXPECT_NEAR(0.6 + 0.23 + 0.15, 0.98, 1e-12); + // Lambda_c (PDG 4122) is intentionally unmodeled -> FF 0. Modeling it must update this. + EXPECT_EQ(xs.FragmentationFraction(static_cast(4122)), 0.0); +} + +// Closure + fragmentation partition must hold across the ANALYSIS energy band +// (TeV-PeV), not only <=300 GeV. Uses a wide total spline (100 GeV - 1 PeV); a +// spline that silently threw or mis-partitioned at PeV would crash or bias the +// production run at exactly the analysis energies. +TEST(PythiaDISCharmClosure, FragmentationClosureAtAnalysisEnergies) { + const char* tt = std::getenv("SIREN_PYTHIA_WIDE_SIGMA"); + if(!tt) GTEST_SKIP() << "Set SIREN_PYTHIA_WIDE_SIGMA (wide total spline, 100 GeV-1 PeV) to run."; + std::vector primaries = { ParticleType::NuMu }; + std::vector targets = { ParticleType::PPlus }; + PythiaDISCrossSection xs(std::string(""), std::string(tt), + 1, 0.9382720813, 1.0, primaries, targets, "", "LHAPDF6:CT18NLO", "cm"); + const double ff_sum = (0.6 + 0.23 + 0.15) / 0.98; + + for(double E : {1.0e4, 1.0e5, 1.0e6}) { // 10 TeV, 100 TeV, 1 PeV + double sigma_incl = 0.0; + ASSERT_NO_THROW(sigma_incl = xs.TotalCrossSection(ParticleType::NuMu, E)) + << "total spline does not cover E=" << E << " GeV"; + ASSERT_GT(sigma_incl, 0.0); + + auto sigs = xs.GetPossibleSignaturesFromParents(ParticleType::NuMu, ParticleType::PPlus); + ASSERT_EQ(sigs.size(), 3u); + double sum = 0.0; + for(auto const & sig : sigs) { + InteractionRecord rec; + rec.signature = sig; + rec.primary_momentum[0] = E; + double v = xs.TotalCrossSection(rec); + sum += v; + ParticleType d = ParticleType::unknown; + for(auto t : sig.secondary_types) if(isD(t)) { d = t; break; } + ASSERT_NE(d, ParticleType::unknown); + EXPECT_NEAR(v, sigma_incl * expected_ff(d), sigma_incl * 1e-9) << "E=" << E; + } + EXPECT_NEAR(sum, sigma_incl * ff_sum, sigma_incl * 1e-9) << "E=" << E; + + InteractionRecord rec; + rec.signature.primary_type = ParticleType::NuMu; + rec.signature.target_type = ParticleType::PPlus; + rec.primary_momentum[0] = E; + double gen = xs.TotalCrossSectionAllFinalStates(rec); + EXPECT_NEAR(gen, sum, sigma_incl * 1e-9) << "generation/physical mismatch at E=" << E; + } +} #endif // SIREN_HAS_PYTHIA8 int main(int argc, char** argv) { diff --git a/tests/python/test_pythia_charm_validation.py b/tests/python/test_pythia_charm_validation.py new file mode 100644 index 000000000..827fd6a5a --- /dev/null +++ b/tests/python/test_pythia_charm_validation.py @@ -0,0 +1,346 @@ +"""Physics validation for the PythiaDISCrossSection charm-DIS generator. + +This is the SIREN-side counterpart of the Pythia-vs-SIREN comparison shown in +the IceCube multi-cascade tau/charm update (Diffuse WG): it guards the absolute +charm-production rate, confirms that SampleFinalState reproduces bare Pythia's +DIS kinematics (Bjorken x/y, Q^2), and that an optional differential spline +covers the realized sampling support (no silent-zero weight bias), across the +analysis energy band (TeV-PeV). + +Everything here needs Pythia8/LHAPDF at runtime and a wide-range total (and, +for the coverage test, differential) charm spline. It is therefore gated behind +environment variables and skips cleanly when they are unset: + + SIREN_PYTHIA_WIDE_SIGMA -> total sigma(E) FITS spline, 100 GeV - 1 PeV + SIREN_PYTHIA_WIDE_DSDXDY -> differential d2sigma/dx dy FITS spline (optional) + LHAPDF_DATA_PATH -> LHAPDF data dir containing the PDF set below + +Generate the splines with siren.pythia_charm_splines (see scratch gen script): + + python gen_wide_splines.py + +The SampleFinalState path re-initializes Pythia per event (~1 s/event), so the +SIREN-sampled statistics are deliberately modest; the bare-Pythia reference +(GeneratePythiaCharmSamples, ~ms/event) uses high statistics. +""" +import math +import os + +import pytest + +siren = pytest.importorskip("siren") + +PDF_SET = os.environ.get("SIREN_PYTHIA_PDF", "LHAPDF6:CT18NLO") +PYTHIA_DATA = os.environ.get("PYTHIA8DATA", "") +M_N = 0.9314943 # isoscalar nucleon mass (GeV) +M_MU = 0.105658 + +_SIGMA = os.environ.get("SIREN_PYTHIA_WIDE_SIGMA") +_DSDXDY = os.environ.get("SIREN_PYTHIA_WIDE_DSDXDY") +_LHAPDF = os.environ.get("LHAPDF_DATA_PATH") + +_have_total = bool(_SIGMA and os.path.exists(_SIGMA) and _LHAPDF) +_have_diff = bool(_DSDXDY and os.path.exists(_DSDXDY)) + +_pythia_reason = ( + "set SIREN_PYTHIA_WIDE_SIGMA (wide total spline) and LHAPDF_DATA_PATH, and " + "build with SIREN_WITH_PYTHIA8=ON, to run the charm validation tests" +) + +# PythiaDISCrossSection is only registered when SIREN is built with Pythia8. +_has_pythia_class = hasattr(getattr(siren, "interactions", object()), "PythiaDISCrossSection") + +pytestmark = pytest.mark.skipif( + not (_have_total and _has_pythia_class), reason=_pythia_reason +) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +def _make_xs(with_differential): + import siren.interactions + import siren.dataclasses + PT = siren.dataclasses.Particle.ParticleType + diff = _DSDXDY if (with_differential and _have_diff) else "" + return siren.interactions.PythiaDISCrossSection( + diff, _SIGMA, 1, M_N, 1.0, [PT.NuMu], [PT.PPlus], + PYTHIA_DATA, PDF_SET, "cm") + + +def _sample_siren(xs, E, n): + """SampleFinalState n charm events at neutrino energy E (GeV). + + Returns a list of dicts with the stored Bjorken (x, y), the finalized D and + primary-lepton 4-momenta, and the D/lepton energy fractions. + """ + import siren.dataclasses + import siren.utilities + PT = siren.dataclasses.Particle.ParticleType + rng = siren.utilities.SIREN_random(20260506) + sigs = list(xs.GetPossibleSignaturesFromParents(PT.NuMu, PT.PPlus)) + out = [] + attempts = 0 + while len(out) < n and attempts < n * 20: + attempts += 1 + sig = sigs[len(out) % len(sigs)] + ir = siren.dataclasses.InteractionRecord() + ir.signature = sig + ir.primary_momentum = [E, 0.0, 0.0, E] + ir.primary_mass = 0.0 + ir.target_mass = M_N + cdr = siren.dataclasses.CrossSectionDistributionRecord(ir) + try: + xs.SampleFinalState(cdr, rng) + except RuntimeError: + continue + params = dict(cdr.interaction_parameters) + ir_out = siren.dataclasses.InteractionRecord() + ir_out.signature = ir.signature + ir_out.primary_momentum = [E, 0.0, 0.0, E] + ir_out.primary_mass = 0.0 + cdr.finalize(ir_out) + secs = list(ir_out.secondary_momenta) + # secondary order is [charged lepton, Hadrons, D meson] + p_lep, p_D = secs[0], secs[2] + out.append(dict(x=params["bjorken_x"], y=params["bjorken_y"], + E=E, p_lep=p_lep, p_D=p_D, + zD=p_D[0] / E, zlep=p_lep[0] / E, + ir_out=ir_out)) + return out + + +def _bare_pythia_xy(E, n_events): + """Bare-Pythia muon-reconstructed (x, y) for an ice p/n mix (10:8).""" + import siren.interactions + PT = siren.interactions.PythiaDISCrossSection + xs_all, ys_all = [], [] + for target_pdg, w in ((2212, 10), (2112, 8)): + _, _, x, y = PT.GeneratePythiaCharmSamples( + 1, 14, target_pdg, M_N, PDF_SET, PYTHIA_DATA, 1.0, + [float(E)], int(n_events * w / 18)) + xs_all += list(x) + ys_all += list(y) + return xs_all, ys_all + + +def _mean(v): + return sum(v) / len(v) + + +# --------------------------------------------------------------------------- +# Test 1: absolute charm-production rate / charm fraction (normalization) +# --------------------------------------------------------------------------- +def test_charm_total_cross_section_normalization(): + """The inclusive charm-DIS sigma must have the right absolute magnitude. + + Pins (a) the charm fraction sigma_charm/sigma_CC at 100 GeV against the + textbook nu-N CC cross section, and (b) sigma_charm/E to the right order of + magnitude across 100 GeV - 1 PeV, with monotonic growth. Guards the rate + that sets S:B and the astro-flux normalization fit -- previously unguarded + (only mocks / relative partitioning existed). + """ + import siren.dataclasses + PT = siren.dataclasses.Particle.ParticleType + xs = _make_xs(with_differential=False) + + # (a) charm fraction at 100 GeV (CC sigma is textbook-linear there). + sigma_cc_ref_100 = 0.677e-38 * 100.0 # cm^2, sigma_CC/E ~ 0.677e-38 cm^2/GeV + sigma_charm_100 = xs.TotalCrossSection(PT.NuMu, 100.0) + frac = sigma_charm_100 / sigma_cc_ref_100 + assert 0.03 < frac < 0.12, ( + f"charm fraction at 100 GeV = {frac:.3f} outside the literature band " + f"[0.03, 0.12] (sigma_charm={sigma_charm_100:.3e} cm^2)") + + # (b) order-of-magnitude of sigma_charm/E + monotonic growth across the band. + energies = [1.0e2, 1.0e3, 1.0e4, 1.0e5, 1.0e6] + sig = [xs.TotalCrossSection(PT.NuMu, E) for E in energies] + for E, s in zip(energies, sig): + assert s > 0.0 + soe = s / E + assert 5e-41 < soe < 5e-39, ( + f"sigma_charm/E = {soe:.3e} cm^2/GeV at E={E:.0e} GeV is out of the " + "expected charm-DIS magnitude band") + for a, b in zip(sig, sig[1:]): + assert b > a, "charm cross section must increase with energy" + + +# --------------------------------------------------------------------------- +# Test 2: SampleFinalState reproduces bare-Pythia DIS kinematics (the slides) +# --------------------------------------------------------------------------- +@pytest.mark.skipif(not PYTHIA_DATA, reason="set PYTHIA8DATA to run the Pythia-sampling tests") +def test_sampling_matches_bare_pythia_at_100gev(): + """SIREN SampleFinalState must reproduce bare Pythia's Bjorken x/y and Q^2. + + Reproduces the production-side panels of the Pythia-vs-pythiaSIREN slide at + E_nu = 100 GeV: SampleFinalState extracts/rotates the Pythia final state and + reconstructs (x, y); GeneratePythiaCharmSamples is the same Pythia config + inline. Their distributions must agree -- a frame/extraction bug would show + here as a mismatch. + """ + E = 100.0 + xs = _make_xs(with_differential=False) + siren_ev = _sample_siren(xs, E, n=80) + assert len(siren_ev) >= 40, f"only {len(siren_ev)} SIREN events sampled" + x_si = [e["x"] for e in siren_ev] + y_si = [e["y"] for e in siren_ev] + + x_py, y_py = _bare_pythia_xy(E, n_events=4000) + assert len(x_py) > 1000 + + # Compare means; tolerance is set by the modest SIREN sample size. + def _stderr(v): + m = _mean(v) + var = sum((vi - m) ** 2 for vi in v) / max(1, len(v) - 1) + return math.sqrt(var / len(v)) + + for name, vi_si, vi_py in (("y", y_si, y_py), ("x", x_si, x_py)): + m_si, m_py = _mean(vi_si), _mean(vi_py) + tol = 4.0 * (_stderr(vi_si) + _stderr(vi_py)) + 0.05 * m_py + assert abs(m_si - m_py) < tol, ( + f"mean Bjorken-{name}: SIREN={m_si:.4f} vs bare-Pythia={m_py:.4f} " + f"(tol {tol:.4f}) -- SampleFinalState does not reproduce Pythia") + + # Sampled kinematics must be physical. + assert all(0.0 < e["x"] < 1.0 for e in siren_ev) + assert all(0.0 < e["y"] < 1.0 for e in siren_ev) + + +# --------------------------------------------------------------------------- +# Test 3: D-meson energy fraction + production collimation (morphology) +# --------------------------------------------------------------------------- +@pytest.mark.skipif(not PYTHIA_DATA, reason="set PYTHIA8DATA to run the Pythia-sampling tests") +def test_d_meson_energy_fraction_and_collimation(): + """The D meson carries a sizeable, physical fraction of the event energy and + is produced nearly collinear with the primary lepton at high energy -- the + morphology that sets the two-cascade energy split and separation direction. + """ + E = 1.0e4 # 10 TeV + xs = _make_xs(with_differential=False) + ev = _sample_siren(xs, E, n=60) + assert len(ev) >= 30 + zD = [e["zD"] for e in ev] + # Every D carries a physical (0, 1) fraction of the neutrino energy. + assert all(0.0 < z < 1.0 for z in zD) + # Mean D energy fraction in a sane charm-DIS range (not ~0, not ~1). + assert 0.05 < _mean(zD) < 0.95 + # Opening angle between D and primary lepton is small at 10 TeV (collimated). + def _angle(a, b): + import math + na = math.sqrt(sum(a[i] ** 2 for i in (1, 2, 3))) + nb = math.sqrt(sum(b[i] ** 2 for i in (1, 2, 3))) + dot = sum(a[i] * b[i] for i in (1, 2, 3)) + c = max(-1.0, min(1.0, dot / (na * nb))) + return math.degrees(math.acos(c)) + angles = [_angle(e["p_D"], e["p_lep"]) for e in ev] + # Median opening angle should be modest (DIS at 10 TeV is forward). + angles.sort() + median = angles[len(angles) // 2] + assert median < 30.0, f"median D/lepton opening angle {median:.1f} deg too large" + + +# --------------------------------------------------------------------------- +# Test 4: optional differential spline covers the sampled support (no silent 0) +# --------------------------------------------------------------------------- +@pytest.mark.skipif(not (_have_diff and PYTHIA_DATA), + reason="set SIREN_PYTHIA_WIDE_DSDXDY + PYTHIA8DATA to run the coverage test") +def test_differential_spline_covers_sampling_support(): + """With a differential spline supplied, DifferentialCrossSection must be + finite-positive on essentially every sampled event, else those events get a + silently-zero physical density and a biased weight. Guards spline (E, x, y) + support vs the realized sampling support at analysis energies. + """ + xs = _make_xs(with_differential=True) + for E in (1.0e3, 1.0e4): # 1 TeV, 10 TeV (within the wide spline x-range) + ev = _sample_siren(xs, E, n=60) + assert len(ev) >= 30, f"too few events at E={E:.0e}" + vals = [xs.DifferentialCrossSection(e["ir_out"]) for e in ev] + good = [v for v in vals if math.isfinite(v) and v > 0.0] + frac = len(good) / len(vals) + assert frac > 0.95, ( + f"only {frac:.3f} of sampled events at E={E:.0e} GeV had a " + "finite-positive differential density -- the differential spline " + "(E, x, y) support does not cover the sampling support (silent-zero " + "weight bias). Widen the spline's logx range.") + + +# --------------------------------------------------------------------------- +# Test 5: end-to-end inject -> weight through the real Injector / Weighter +# --------------------------------------------------------------------------- +@pytest.mark.skipif(not PYTHIA_DATA, reason="set PYTHIA8DATA to run the Pythia-sampling tests") +def test_end_to_end_inject_and_weight(): + """Drive PythiaDISCrossSection as the primary charm-DIS generator through the + real _Injector / _Weighter on the CCM detector. Every generated event must + receive a finite, positive weight, and the weights must vary (genuine + per-event sampling). This exercises the full production-side weight + accumulation (TotalCrossSection interaction depth / position / survival times + the cancelling constant FinalStateProbability) end to end -- the cross-section + closure tests prove the per-vertex factors; this proves they integrate. + """ + import numpy as np + from siren import (dataclasses as dc, injection, interactions, distributions, + detector, math as smath, utilities, _util) + PT = dc.Particle.ParticleType + NuMu = PT.NuMu + + det_dir = _util.get_detector_model_path("CCM") + dm = detector.DetectorModel() + dm.LoadMaterialModel(os.path.join(det_dir, "materials.dat")) + dm.LoadDetectorModel(os.path.join(det_dir, "densities.dat")) + + xs = interactions.PythiaDISCrossSection( + "", _SIGMA, 1, M_N, 1.0, [NuMu], [PT.Ar40Nucleus], PYTHIA_DATA, PDF_SET, "cm") + int_col = interactions.InteractionCollection(NuMu, [xs]) + + pinj = injection.PrimaryInjectionProcess() + pinj.primary_type = NuMu + pinj.interactions = int_col + pinj.distributions = [ + distributions.PrimaryMass(0), + distributions.Monoenergetic(1000.0), # 1 TeV nu_mu + distributions.IsotropicDirection(), + distributions.PointSourcePositionDistribution(smath.Vector3D(0, 0, 0), 5.0), + ] + pphys = injection.PhysicalProcess() + pphys.primary_type = NuMu + pphys.interactions = int_col + pphys.distributions = [distributions.PrimaryMass(0), distributions.IsotropicDirection()] + + rand = utilities.SIREN_random(7) + N = 4 + inj = injection._Injector(N, dm, pinj, rand) + weighter = injection._Weighter([inj], dm, pphys) + + weights = [] + for _ in range(N): + ev = inj.GenerateEvent() + w = weighter.EventWeight(ev) + assert np.isfinite(w) and w > 0.0, f"non-finite/non-positive event weight {w}" + weights.append(w) + + assert len(weights) == N + assert len(set(weights)) > 1, "all event weights identical -- sampling did not vary" + + +# --------------------------------------------------------------------------- +# Test 6: per-event SampleFinalState cost (production throughput guard) +# --------------------------------------------------------------------------- +@pytest.mark.skipif(not PYTHIA_DATA, reason="set PYTHIA8DATA to run the Pythia-sampling tests") +def test_sample_final_state_perf_budget(): + """PythiaDISCrossSection re-initializes Pythia per event (variable-energy mode + is unsupported for WeakBosonExchange). Guard the per-event cost so a major + regression that would make PeV production infeasible is caught. Generous + ceiling; the measured value is reported. + """ + import time + xs = _make_xs(with_differential=False) + M = 10 + t0 = time.time() + ev = _sample_siren(xs, 1.0e4, n=M) + elapsed = time.time() - t0 + assert len(ev) >= M // 2, "too few events sampled to time" + per_event = elapsed / len(ev) + print(f"\nSampleFinalState mean per-event cost: {per_event:.3f} s") + assert per_event < 3.0, ( + f"SampleFinalState is {per_event:.2f} s/event (> 3 s ceiling) -- a " + "Pythia re-init regression would make large-scale production infeasible.") From 745846666e94b17a9ffe88da638a043c1de98d4e Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 21:03:59 -0700 Subject: [PATCH 84/93] Document charm-DIS usage and add a Pythia spline generator README_charm.md describes the two production paths, the runtime requirements, spline generation, decay coverage, and known limitations. generate_charm_pythia_splines.py is a CLI around siren.pythia_charm_splines that rebuilds the total and optional differential splines over a configurable energy grid. --- resources/examples/example1/README_charm.md | 83 +++++++++++++++++++ .../example1/generate_charm_pythia_splines.py | 80 ++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 resources/examples/example1/README_charm.md create mode 100644 resources/examples/example1/generate_charm_pythia_splines.py diff --git a/resources/examples/example1/README_charm.md b/resources/examples/example1/README_charm.md new file mode 100644 index 000000000..133daecba --- /dev/null +++ b/resources/examples/example1/README_charm.md @@ -0,0 +1,83 @@ +# Charm-DIS production in SIREN + +SIREN can produce charmed hadrons from neutrino deep-inelastic scattering and +decay them semileptonically -- the production chain behind the IceCube diffuse +multi-cascade tau/charm search. There are two interchangeable production paths; +both emit a `D` meson directly as a DIS secondary (no separate hadronization +vertex), which a `CharmMesonDecay` secondary process then decays. + +## Two production paths + +| | `PythiaDISCrossSection` | `QuarkDISFromSpline` | +|---|---|---| +| Final state | sampled live from **Pythia8** | analytic slow-rescaling `(xi, y)` | +| Rate | total `sigma(E)` spline (**required**) | total `sigma` spline | +| Differential | **optional** (`d2sigma/dx dy` spline) | required (`dsdxidy` spline) | +| Reweightable | unbiased-only by default; reweightable if a differential spline is supplied | fully reweightable | +| Current | **charged-current only** | CC and NC | +| Use it for | matching Pythia's full hadronization exactly | a fast, fully analytic, reweightable generator | + +`PythiaDISCrossSection` is "Pythia as the integrated generator": `SampleFinalState` +is pure Pythia, and the splines are only the rate/weighting side. When no +differential spline is supplied, `FinalStateProbability` returns a constant that +cancels in the unbiased weight (only the total cross section matters); supply a +differential spline to get a real `dsigma/sigma` density for reweighting. + +## Runtime requirements (`PythiaDISCrossSection`) + +`SampleFinalState` needs Pythia8 + LHAPDF at runtime. The total-cross-section / +weighting path does **not** (it only reads the splines). + +1. Build SIREN with `-DSIREN_WITH_PYTHIA8=ON`. +2. `export LHAPDF_DATA_PATH=` -- the parent of the installed PDF-set folders. + The requested `pdf_set` (default `LHAPDF6:HERAPDF20_NLO_EIG`) **must be + installed there**, or construction raises a clear error naming the set. Any + installed LHAPDF set works (e.g. `LHAPDF6:CT18NLO`). +3. `export PYTHIA8DATA=/share/Pythia8/xmldoc`. +4. macOS: `export DYLD_LIBRARY_PATH=$PREFIX/lib:/lib:$DYLD_LIBRARY_PATH`. + +NC (`interaction_type=2`) is rejected at construction: Z exchange does not force +charm in Pythia. Use `QuarkDISFromSpline` for NC charm. + +## Generating the Pythia splines + +The Pythia splines are not committed (they depend on the PDF set and Pythia +version). Regenerate them from the same Pythia config the sampler uses: + +```bash +python generate_charm_pythia_splines.py --out ./splines \ + --pdf LHAPDF6:CT18NLO --emin 100 --emax 1e6 --npoints 17 +``` + +This writes `pythia_charm_sigma.fits` (always) and `pythia_charm_dsdxdy.fits` +(unless `--no-differential`). The default grid (100 GeV - 1 PeV) covers the +diffuse analysis band. See `siren.pythia_charm_splines` for the library API. + +The slow-rescaling `QuarkDISFromSpline` `dsdxidy_/sigma_nu-N-{cc,nc}-charm-*.fits` +splines are external analysis inputs (LHAPDF-derived, not produced by SIREN); +point `SIREN_CHARM_SPLINE_DIR` at them to run `DIS_IceCube_charm.py` and the +`test_quarkdis_slow_rescaling.py` tests. + +## Charmed-hadron decay + +`CharmMesonDecay` (the 2-body class used by the chain) handles `D0`, `D+`, `Ds` +and their anti-flavors: `D+/-` and `D0/D0bar` via `K`/`K*(892)` semileptonic +modes with a V-A matrix element, and `Ds` via `eta/eta'/phi` pure phase space. +`CharmMesonDecay3Body` is an alternate D0/D+-only implementation (it rejects +other species at construction). `Decay.TotalDecayLength(record)` returns the lab +decay length `beta*gamma*c*tau` **in meters** -- the cascade separation the +Taupede reconstruction targets. + +Every D species a production cross section emits **must** have a registered +secondary decay process, or the injector silently drops it (see the comment in +`DIS_IceCube_charm.py`). The fragmentation fractions (`D0:D+/-:Ds = 0.60:0.23:0.15`, +renormalized /0.98) fold the unmodeled `Lambda_c` into the D mesons. + +## Known limitations / deferred work + +- **NC charm** is not supported by `PythiaDISCrossSection` (use `QuarkDISFromSpline`). +- **`Lambda_c`** (~0.09 raw fraction) is folded into the D mesons, not modeled + as a distinct species (it has a different lifetime and semileptonic mix). +- **Per-event Pythia re-init** (~0.1-1 s/event): variable-energy mode is + unsupported for `WeakBosonExchange`, so each event rebuilds Pythia at its beam + energy. Fine for analysis-scale samples; a throughput item for very large runs. diff --git a/resources/examples/example1/generate_charm_pythia_splines.py b/resources/examples/example1/generate_charm_pythia_splines.py new file mode 100644 index 000000000..10049af3d --- /dev/null +++ b/resources/examples/example1/generate_charm_pythia_splines.py @@ -0,0 +1,80 @@ +"""Generate the Pythia charm-DIS splines used by PythiaDISCrossSection. + +PythiaDISCrossSection samples the charm-DIS final state directly from Pythia and +uses photospline FITS tables only for the cross-section *rate* (a 1D total +sigma(E), always required) and, optionally, a 3D differential d2sigma/dx dy that +lets FinalStateProbability report a real density for reweighting. The splines +are built from the SAME Pythia configuration the sampler uses, so they close +against it. Pythia does NOT read these splines and the splines are NOT committed +to the repo (they depend on the PDF set and Pythia version) -- regenerate them +locally with this script. + +Requirements (see README_charm.md): + * SIREN built with -DSIREN_WITH_PYTHIA8=ON + * LHAPDF_DATA_PATH set, with the requested PDF set installed + * PYTHIA8DATA pointing at the Pythia xmldoc directory + * DYLD_LIBRARY_PATH (macOS) including the prefix lib + the Pythia lib + +Usage: + python generate_charm_pythia_splines.py --out ./splines \ + --pdf LHAPDF6:CT18NLO --emin 100 --emax 1e6 --npoints 17 + +The default energy grid (100 GeV - 1 PeV) covers the IceCube diffuse +multi-cascade charm analysis band. +""" +import argparse +import os + +import numpy as np + +import siren.pythia_charm_splines as pcs + +# nu / target PDG defaults (charged-current nu_mu on an isoscalar nucleon). +ISO_MASS = 0.9314943 # GeV + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--out", default=".", help="output directory") + ap.add_argument("--pdf", default="LHAPDF6:CT18NLO", help="LHAPDF set (must be installed)") + ap.add_argument("--interaction-type", type=int, default=1, help="1=CC (only CC is supported)") + ap.add_argument("--primary-pdg", type=int, default=14, help="primary neutrino PDG (e.g. 14 = nu_mu)") + ap.add_argument("--target-pdg", type=int, default=2212, help="target nucleon PDG (2212 p / 2112 n)") + ap.add_argument("--emin", type=float, default=100.0, help="min neutrino energy [GeV]") + ap.add_argument("--emax", type=float, default=1.0e6, help="max neutrino energy [GeV]") + ap.add_argument("--npoints", type=int, default=17, help="log-spaced energy points") + ap.add_argument("--minimum-q2", type=float, default=1.0, help="Pythia PhaseSpace:Q2Min [GeV^2]") + ap.add_argument("--n-total", type=int, default=4000, help="events/energy for the total spline") + ap.add_argument("--n-diff", type=int, default=20000, help="events/energy for the differential spline") + ap.add_argument("--no-differential", action="store_true", help="only build the total spline") + args = ap.parse_args() + + if not os.environ.get("LHAPDF_DATA_PATH"): + raise SystemExit("LHAPDF_DATA_PATH is not set (parent of the PDF set folders).") + pythia_data = os.environ.get("PYTHIA8DATA", "") + os.makedirs(args.out, exist_ok=True) + + energies = np.logspace(np.log10(args.emin), np.log10(args.emax), args.npoints).tolist() + print("energies (GeV):", [round(e, 1) for e in energies], flush=True) + + sigma_out = os.path.join(args.out, "pythia_charm_sigma.fits") + pcs.generate_total_spline( + sigma_out, interaction_type=args.interaction_type, primary_pdg=args.primary_pdg, + target_pdg=args.target_pdg, target_mass=ISO_MASS, pdf_set=args.pdf, + pythia_data_path=pythia_data, energies=energies, minimum_Q2=args.minimum_q2, + n_events=args.n_total) + print("wrote", sigma_out, flush=True) + + if not args.no_differential: + diff_out = os.path.join(args.out, "pythia_charm_dsdxdy.fits") + pcs.generate_differential_spline( + diff_out, interaction_type=args.interaction_type, primary_pdg=args.primary_pdg, + target_pdg=args.target_pdg, target_mass=ISO_MASS, pdf_set=args.pdf, + pythia_data_path=pythia_data, energies=energies, minimum_Q2=args.minimum_q2, + n_events=args.n_diff) + print("wrote", diff_out, flush=True) + + +if __name__ == "__main__": + main() From 727355684b9230ca34ac751b8237562af8fc70fa Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 22:16:52 -0700 Subject: [PATCH 85/93] Raise on out-of-range differential cross section instead of returning 0 A sampled event outside the differential spline support was assigned zero density, biasing its weight to zero. PythiaDISCrossSection and QuarkDISFromSpline DifferentialCrossSection now raise (naming the point and the spline range) when the energy or (x/xi, y) point is outside the grid or no differential spline is loaded; physics-threshold cuts still return 0. --- .../private/PythiaDISCrossSection.cxx | 27 ++++++++++++++----- .../private/QuarkDISFromSpline.cxx | 23 ++++++++++------ .../test/PythiaDISCharmClosure_TEST.cxx | 19 +++++++++++++ 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/projects/interactions/private/PythiaDISCrossSection.cxx b/projects/interactions/private/PythiaDISCrossSection.cxx index cb321cedd..7701db20b 100644 --- a/projects/interactions/private/PythiaDISCrossSection.cxx +++ b/projects/interactions/private/PythiaDISCrossSection.cxx @@ -373,6 +373,8 @@ double PythiaDISCrossSection::TotalCrossSection(siren::dataclasses::ParticleType double PythiaDISCrossSection::DifferentialCrossSection(dataclasses::InteractionRecord const & interaction) const { + if(!has_differential_) + throw std::runtime_error("PythiaDISCrossSection::DifferentialCrossSection called but no differential spline was loaded (the unbiased default uses a constant FinalStateProbability)."); rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); rk::P4 p2(geom3::Vector3(0, 0, 0), interaction.target_mass); double primary_energy = interaction.primary_momentum[0]; @@ -422,19 +424,32 @@ double PythiaDISCrossSection::DifferentialCrossSection(dataclasses::InteractionR } double PythiaDISCrossSection::DifferentialCrossSection(double energy, double x, double y, double secondary_lepton_mass, double Q2) const { + if(!has_differential_) + throw std::runtime_error("PythiaDISCrossSection::DifferentialCrossSection called but no differential spline was loaded (the unbiased default uses a constant FinalStateProbability)."); double log_energy = log10(energy); + // Out of spline SUPPORT -> raise, never silently return 0: a silent zero on a + // genuinely sampled event would bias that event's physical density (and hence + // its weight) to zero. The energy extent and the (x, y) grid define validity. if(log_energy < differential_cross_section_.lower_extent(0) || log_energy > differential_cross_section_.upper_extent(0)) - return 0.0; - if(x <= 0 || x >= 1) return 0.0; - if(y <= 0 || y >= 1) return 0.0; + throw std::runtime_error("PythiaDISCrossSection: energy " + std::to_string(energy) + + " GeV is outside the differential spline energy range [" + + std::to_string(std::pow(10.0, differential_cross_section_.lower_extent(0))) + ", " + + std::to_string(std::pow(10.0, differential_cross_section_.upper_extent(0))) + + "] GeV; regenerate the differential spline to cover this energy."); + if(x <= 0 || x >= 1 || y <= 0 || y >= 1) + throw std::runtime_error("PythiaDISCrossSection: unphysical Bjorken (x=" + + std::to_string(x) + ", y=" + std::to_string(y) + ") outside (0, 1)."); if(std::isnan(Q2)) Q2 = 2.0 * energy * target_mass_ * x * y; - if(Q2 < minimum_Q2_) return 0; - (void)secondary_lepton_mass; // analytic charm-DIS gate removed (see above) + if(Q2 < minimum_Q2_) return 0; // below the configured Q^2 cut: a modeling threshold, not a spline-range miss + (void)secondary_lepton_mass; // analytic charm-DIS gate removed (see above) std::array coordinates{{log_energy, log10(x), log10(y)}}; std::array centers; - if(!differential_cross_section_.searchcenters(coordinates.data(), centers.data())) return 0; + if(!differential_cross_section_.searchcenters(coordinates.data(), centers.data())) + throw std::runtime_error("PythiaDISCrossSection: Bjorken (x=" + std::to_string(x) + + ", y=" + std::to_string(y) + ") at E=" + std::to_string(energy) + + " GeV is outside the differential spline (x, y) grid; widen the spline's logx/logy range."); double result = pow(10., differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0)); return unit * result; } diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 79669428b..751f025f5 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -14,6 +14,7 @@ #include #include #include +#include #include #include // for P4, Boost @@ -573,16 +574,20 @@ double QuarkDISFromSpline::DifferentialCrossSection(dataclasses::InteractionReco double QuarkDISFromSpline::DifferentialCrossSection(double energy, double xi, double y, double secondary_lepton_mass, double Q2) const { double log_energy = log10(energy); - // check preconditions + // Out of spline SUPPORT -> raise, never silently return 0: a silent zero on a + // genuinely sampled event would bias that event's physical density (and hence + // its weight) to zero. if (log_energy < differential_cross_section_.lower_extent(0) || log_energy > differential_cross_section_.upper_extent(0)) { - return 0.0; - } - if (xi <= 0 || xi >= 1) { - return 0.0; + throw std::runtime_error("QuarkDISFromSpline: energy " + std::to_string(energy) + + " GeV is outside the differential spline energy range [" + + std::to_string(std::pow(10.0, differential_cross_section_.lower_extent(0))) + ", " + + std::to_string(std::pow(10.0, differential_cross_section_.upper_extent(0))) + + "] GeV."); } - if (y <= 0 || y >= 1) { - return 0.0; + if (xi <= 0 || xi >= 1 || y <= 0 || y >= 1) { + throw std::runtime_error("QuarkDISFromSpline: unphysical (xi=" + + std::to_string(xi) + ", y=" + std::to_string(y) + ") outside (0, 1)."); } if (std::isnan(Q2)) { @@ -598,7 +603,9 @@ double QuarkDISFromSpline::DifferentialCrossSection(double energy, double xi, do std::array coordinates{{log_energy, log10(xi), log10(y)}}; std::array centers; if (!differential_cross_section_.searchcenters(coordinates.data(), centers.data())) { - return 0; + throw std::runtime_error("QuarkDISFromSpline: (xi=" + std::to_string(xi) + + ", y=" + std::to_string(y) + ") at E=" + std::to_string(energy) + + " GeV is outside the differential spline (xi, y) grid."); } double result = pow(10., differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0)); assert(result >= 0); diff --git a/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx b/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx index fc967c32b..eb2218545 100644 --- a/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx +++ b/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx @@ -237,6 +237,25 @@ TEST(PythiaDISCharmClosure, FragmentationClosureAtAnalysisEnergies) { EXPECT_NEAR(gen, sum, sigma_incl * 1e-9) << "generation/physical mismatch at E=" << E; } } + +// Out-of-range differential evaluation must RAISE, not silently return 0 (a +// silent zero on a sampled event biases its weight). Needs a differential spline. +TEST(PythiaDISCharmClosure, DifferentialOutOfRangeRaises) { + const char* dd = std::getenv("SIREN_PYTHIA_TEST_DSDXDY"); + const char* tt = std::getenv("SIREN_PYTHIA_TEST_SIGMA"); + if(!dd || !tt) GTEST_SKIP() << "Set SIREN_PYTHIA_TEST_DSDXDY and SIREN_PYTHIA_TEST_SIGMA to run."; + std::vector primaries = { ParticleType::NuMu }; + std::vector targets = { ParticleType::PPlus }; + PythiaDISCrossSection xs(std::string(dd), std::string(tt), + 1, 0.9382720813, 1.0, primaries, targets, "", "LHAPDF6:CT18NLO", "cm"); + + // In range -> finite positive density (Q2 computed from x,y). + EXPECT_GT(xs.DifferentialCrossSection(100.0, 0.1, 0.5, 0.105), 0.0); + // Energy outside the spline extent must raise. + EXPECT_THROW(xs.DifferentialCrossSection(1.0e12, 0.1, 0.5, 0.105), std::runtime_error); + // (x, y) outside the spline grid must raise (explicit Q2 bypasses the Q2 cut). + EXPECT_THROW(xs.DifferentialCrossSection(100.0, 1.0e-8, 0.5, 0.105, 5.0), std::runtime_error); +} #endif // SIREN_HAS_PYTHIA8 int main(int argc, char** argv) { From 718f63f55d403e43e754f5d6983cb1782783fc41 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 22:16:52 -0700 Subject: [PATCH 86/93] Add quantitative rate closure, out-of-range coverage, and QuarkDIS normalization tests The end-to-end test in test_pythia_charm_validation asserts EventWeight == InteractionProbability / N per event and sum(EventWeight) == mean(P_int), bounded by a thin-target sigma*n*L estimate. test_quarkdis_slow_rescaling adds a charm-fraction normalization test against the literature band. --- tests/python/test_pythia_charm_validation.py | 84 +++++++++++++++----- tests/python/test_quarkdis_slow_rescaling.py | 38 +++++++++ 2 files changed, 104 insertions(+), 18 deletions(-) diff --git a/tests/python/test_pythia_charm_validation.py b/tests/python/test_pythia_charm_validation.py index 827fd6a5a..2526f67c4 100644 --- a/tests/python/test_pythia_charm_validation.py +++ b/tests/python/test_pythia_charm_validation.py @@ -254,28 +254,48 @@ def test_differential_spline_covers_sampling_support(): for E in (1.0e3, 1.0e4): # 1 TeV, 10 TeV (within the wide spline x-range) ev = _sample_siren(xs, E, n=60) assert len(ev) >= 30, f"too few events at E={E:.0e}" - vals = [xs.DifferentialCrossSection(e["ir_out"]) for e in ev] - good = [v for v in vals if math.isfinite(v) and v > 0.0] - frac = len(good) / len(vals) + in_range = 0 + out_of_range = 0 + silent_zero = 0 + for e in ev: + try: + v = xs.DifferentialCrossSection(e["ir_out"]) + except RuntimeError: + out_of_range += 1 # out-of-spline-support correctly RAISES + continue + if math.isfinite(v) and v > 0.0: + in_range += 1 + else: + silent_zero += 1 + # The new contract: out-of-range raises; nothing returns a silent zero. + assert silent_zero == 0, ( + f"{silent_zero} sampled events at E={E:.0e} GeV returned a silent-zero " + "differential density instead of raising") + frac = in_range / len(ev) assert frac > 0.95, ( - f"only {frac:.3f} of sampled events at E={E:.0e} GeV had a " - "finite-positive differential density -- the differential spline " - "(E, x, y) support does not cover the sampling support (silent-zero " - "weight bias). Widen the spline's logx range.") + f"only {frac:.3f} of sampled events at E={E:.0e} GeV fell inside the " + "differential spline (E, x, y) support; widen the spline's logx/logy range.") # --------------------------------------------------------------------------- # Test 5: end-to-end inject -> weight through the real Injector / Weighter # --------------------------------------------------------------------------- @pytest.mark.skipif(not PYTHIA_DATA, reason="set PYTHIA8DATA to run the Pythia-sampling tests") -def test_end_to_end_inject_and_weight(): - """Drive PythiaDISCrossSection as the primary charm-DIS generator through the - real _Injector / _Weighter on the CCM detector. Every generated event must - receive a finite, positive weight, and the weights must vary (genuine - per-event sampling). This exercises the full production-side weight - accumulation (TotalCrossSection interaction depth / position / survival times - the cancelling constant FinalStateProbability) end to end -- the cross-section - closure tests prove the per-vertex factors; this proves they integrate. +def test_end_to_end_rate_closure(): + """Quantitative inject->weight rate closure for charm DIS through the real + _Injector / _Weighter on the CCM detector. + + SIREN folds 1/N into EventWeight and (with injection==physical so the shared + distributions and the cross-section probability cancel, and the injection-side + PointSource position propagator cancels the physical position propagator) + leaves EventWeight_i = InteractionProbability_i / N. Therefore: + - per event, EventWeight must equal GetInteractionProbabilities/N (machine + precision; EventWeight and GetInteractionProbabilities are independent code + paths, so their agreement IS the unbiasedness proof -- a charm + SampleFinalState/FinalStateProbability/TotalCrossSection inconsistency, the + PR#74 closure-break class, would break it), and + - sum(EventWeight) is the unbiased physical-rate estimator == mean(P_int), + which a thin-target estimate sigma * n_Ar * L_eff bounds from above/below. """ import numpy as np from siren import (dataclasses as dc, injection, interactions, distributions, @@ -307,20 +327,48 @@ def test_end_to_end_inject_and_weight(): pphys.distributions = [distributions.PrimaryMass(0), distributions.IsotropicDirection()] rand = utilities.SIREN_random(7) - N = 4 + E0 = 1000.0 # 1 TeV monoenergetic + pinj.distributions = [ + distributions.PrimaryMass(0), + distributions.Monoenergetic(E0), + distributions.IsotropicDirection(), + distributions.PointSourcePositionDistribution(smath.Vector3D(0, 0, 0), 5.0), + ] + N = 10 inj = injection._Injector(N, dm, pinj, rand) weighter = injection._Weighter([inj], dm, pphys) - weights = [] + weights, int_probs = [], [] for _ in range(N): ev = inj.GenerateEvent() w = weighter.EventWeight(ev) + ip = weighter.GetInteractionProbabilities(ev, 0)[0] assert np.isfinite(w) and w > 0.0, f"non-finite/non-positive event weight {w}" weights.append(w) + int_probs.append(ip) + + # (1) Per-event unbiasedness: EventWeight == single-event interaction probability / N. + for w, ip in zip(weights, int_probs): + assert abs(w - ip / N) <= 1e-9 * (ip / N), f"EventWeight {w} != P_int/N {ip / N}" - assert len(weights) == N + # (2) Sum of weights is the unbiased physical-rate estimator == mean(P_int). + sum_w = sum(weights) + mean_ip = sum(int_probs) / N + assert abs(sum_w - mean_ip) <= 1e-9 * mean_ip + + # (3) finite, positive, genuinely varying. assert len(set(weights)) > 1, "all event weights identical -- sampling did not vary" + # (4) Analytic thin-target anchor: sum_w ~ sigma_charm * n_Ar * L_eff, with the + # effective LAr path L_eff between ~0 and the 5 m injection cap (isotropic rays + # exit the volume). sigma is the per-nucleon charm spline value at E0. + sigma_cm2 = xs.TotalCrossSection(NuMu, E0) + n_Ar = 1.396 * 6.022e23 / 40.0 # CCM liquid-argon number density [cm^-3] + L_max_cm = 500.0 + upper = sigma_cm2 * n_Ar * L_max_cm + assert 0.0 < sum_w < upper, f"sum_w {sum_w:.3e} not in (0, sigma*n*L_max={upper:.3e})" + assert sum_w > 0.02 * upper, f"sum_w {sum_w:.3e} implausibly small vs thin-target {upper:.3e}" + # --------------------------------------------------------------------------- # Test 6: per-event SampleFinalState cost (production throughput guard) diff --git a/tests/python/test_quarkdis_slow_rescaling.py b/tests/python/test_quarkdis_slow_rescaling.py index ffbaa007d..66dcdecb8 100644 --- a/tests/python/test_quarkdis_slow_rescaling.py +++ b/tests/python/test_quarkdis_slow_rescaling.py @@ -356,3 +356,41 @@ def test_quarkdis_sample_density_closure(charm_xs, signature, rng): checked += 1 assert checked > 0, "no positive-dxs events were available for closure check" + + +# --------------------------------------------------------------------------- +# Test 4: absolute charm-DIS normalization (charm fraction vs literature/Pythia) +# --------------------------------------------------------------------------- +def test_quarkdis_charm_fraction_normalization(): + """The inclusive slow-rescaling charm-CC cross section must have the right + absolute magnitude: the charm fraction sigma_charm / sigma_CC must land in the + literature band (~4-7% over 100 GeV - 1 TeV), cross-validating the independent + PythiaDISCrossSection charm fraction (~6.5% at 100 GeV). The spline is read in + cm so TotalCrossSection returns cm^2 (the per-nucleon reference below is cm^2). + """ + import siren.interactions + import siren.dataclasses + + PT = siren.dataclasses.Particle.ParticleType + xs = siren.interactions.QuarkDISFromSpline( + _DIFF_FILE, _TOTAL_FILE, int(1), M_N, int(1), + [PT.NuMu], [PT.O16Nucleus], "cm") + + sigma_cc_per_gev = 0.677e-38 # textbook nu-N CC sigma/E [cm^2/GeV] + prev = None + n_checked = 0 + for E in (100.0, 200.0, 300.0): + try: + s = xs.TotalCrossSection(PT.NuMu, E) + except RuntimeError: + continue # energy outside the provided spline range + assert s > 0.0 + frac = s / (sigma_cc_per_gev * E) + assert 0.02 < frac < 0.10, ( + f"charm fraction {frac:.3f} at {E:.0f} GeV outside the literature band " + "[0.02, 0.10]") + if prev is not None: + assert s > prev, "charm cross section must increase with energy" + prev = s + n_checked += 1 + assert n_checked >= 1, "no in-range energy point was available to normalize" From bafdcab9434ce1c971cda278678cd9eb2b392d29 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sat, 27 Jun 2026 22:16:52 -0700 Subject: [PATCH 87/93] Add charm slow-rescaling spline generator and dimuon kinematics example generate_charm_slowrescaling_splines.cpp and fit_charm_slowrescaling_splines.py build reference slow-rescaling charm splines from any installed LHAPDF set (LO slow rescaling, PDFs at xi, Q2 = 2 M E y xi - mc^2), so QuarkDISFromSpline can run without external CT14 splines. reproduce_dimuon_kinematics.py runs the full DIS -> D -> muon chain at 100 GeV and saves the dimuon kinematic observables. --- resources/examples/example1/README_charm.md | 26 +++- .../fit_charm_slowrescaling_splines.py | 70 +++++++++ .../generate_charm_slowrescaling_splines.cpp | 92 ++++++++++++ .../example1/reproduce_dimuon_kinematics.py | 134 ++++++++++++++++++ 4 files changed, 319 insertions(+), 3 deletions(-) create mode 100644 resources/examples/example1/fit_charm_slowrescaling_splines.py create mode 100644 resources/examples/example1/generate_charm_slowrescaling_splines.cpp create mode 100644 resources/examples/example1/reproduce_dimuon_kinematics.py diff --git a/resources/examples/example1/README_charm.md b/resources/examples/example1/README_charm.md index 133daecba..b588639bf 100644 --- a/resources/examples/example1/README_charm.md +++ b/resources/examples/example1/README_charm.md @@ -54,9 +54,29 @@ This writes `pythia_charm_sigma.fits` (always) and `pythia_charm_dsdxdy.fits` diffuse analysis band. See `siren.pythia_charm_splines` for the library API. The slow-rescaling `QuarkDISFromSpline` `dsdxidy_/sigma_nu-N-{cc,nc}-charm-*.fits` -splines are external analysis inputs (LHAPDF-derived, not produced by SIREN); -point `SIREN_CHARM_SPLINE_DIR` at them to run `DIS_IceCube_charm.py` and the -`test_quarkdis_slow_rescaling.py` tests. +splines are the analysis's external inputs (Dutta-Kim CT14 tooling). For a +physically-validated *reference* set from any installed LHAPDF set, build and run +`generate_charm_slowrescaling_splines.cpp` (LO slow rescaling: PDFs at the parton +fraction xi, `Q^2 = 2 M E y xi - m_c^2`) then `fit_charm_slowrescaling_splines.py`. +The integrated charm fraction is ~4% at 100 GeV rising to ~6% at 10 TeV -- the +literature band, cross-checking the Pythia charm fraction ~6.5%. Point +`SIREN_CHARM_SPLINE_DIR` at either set to run `DIS_IceCube_charm.py` and +`test_quarkdis_slow_rescaling.py` (kinematic bounds, sample==density closure, and +the charm-fraction normalization). + +Out-of-range events raise: if a sampled event falls outside the differential +spline's `(E, x/xi, y)` support, `DifferentialCrossSection` raises (it never +silently returns 0, which would bias that event's weight). Regenerate the spline +wider, or check why the event is out of range. + +## Validating against the Pythia-vs-pythiaSIREN slides + +`reproduce_dimuon_kinematics.py` runs the full chain (PythiaDIS DIS -> D -> +`CharmMesonDecay` muon channel) at E_nu = 100 GeV and saves the five slide +observables (mu-pair opening angle, semileptonic E_mu/E_D, Bjorken-y, D/mu opening +angle, Q^2). They reproduce the slide histogram shapes; the committed regression +`tests/python/test_pythia_charm_validation.py` checks SampleFinalState against bare +Pythia quantitatively. ## Charmed-hadron decay diff --git a/resources/examples/example1/fit_charm_slowrescaling_splines.py b/resources/examples/example1/fit_charm_slowrescaling_splines.py new file mode 100644 index 000000000..059a6d3c5 --- /dev/null +++ b/resources/examples/example1/fit_charm_slowrescaling_splines.py @@ -0,0 +1,70 @@ +"""Fit the slow-rescaling charm grid (from generate_charm_slowrescaling_splines) +into the FITS splines QuarkDISFromSpline consumes. + +Reads xidiff.txt (3D log10 d2sigma/dxi dy on a log10(E), log10(xi), log10(y) grid) +and xisigma.txt (1D total sigma vs E), and writes + + dsdxidy_nu-N-cc-charm-CT14nlo_central.fits (3D differential) + sigma_nu-N-cc-charm-CT14nlo_central.fits (1D total, cm^2) + +Point SIREN_CHARM_SPLINE_DIR at the output directory to run +tests/python/test_quarkdis_slow_rescaling.py (kinematic bounds, differential +positivity, sample==density closure, and the charm-fraction normalization). +The differential stores log10(d2sigma/dxi dy) directly (QuarkDISFromSpline's +DifferentialCrossSection returns 10**value); the total stores log10(sigma_cm2). +""" +import os +import sys + +import numpy as np +import photospline + + +def knots(c, order=2): + c = np.asarray(c, float) + d = c[1] - c[0] + return np.concatenate([c[0] - d * np.arange(order, 0, -1), c, + c[-1] + d * np.arange(1, order + 1)]) + + +def main(): + src = sys.argv[1] if len(sys.argv) > 1 else "." + out = sys.argv[2] if len(sys.argv) > 2 else "." + + with open(os.path.join(src, "xidiff.txt")) as f: + nE, nxi, ny = map(int, f.readline().split()) + Ev = [float(f.readline()) for _ in range(nE)] + lxi = [float(f.readline()) for _ in range(nxi)] + ly = [float(f.readline()) for _ in range(ny)] + Z = np.full((nE, nxi, ny), -99.0) + W = np.zeros((nE, nxi, ny)) + for i in range(nE): + for j in range(nxi): + for k in range(ny): + v, w = f.readline().split() + Z[i, j, k] = float(v) + W[i, j, k] = float(w) + + logE = np.log10(Ev) + z, w = photospline.ndsparse.from_data(Z, W) + sp = photospline.glam_fit( + z, w, [logE, np.array(lxi), np.array(ly)], + [knots(logE), knots(lxi), knots(ly)], [2, 2, 2], [1e-2, 1e-2, 1e-2], [2, 2, 2]) + diff_out = os.path.join(out, "dsdxidy_nu-N-cc-charm-CT14nlo_central.fits") + sp.write(diff_out) + + E2, sig = np.loadtxt(os.path.join(src, "xisigma.txt"), unpack=True) + zt, wt = photospline.ndsparse.from_data(np.log10(sig), np.ones_like(sig)) + spt = photospline.glam_fit( + zt, wt, [np.log10(E2)], [knots(np.log10(E2))], [2], [1e-3], [2]) + tot_out = os.path.join(out, "sigma_nu-N-cc-charm-CT14nlo_central.fits") + spt.write(tot_out) + + i100 = int(np.argmin(np.abs(E2 - 100.0))) + print(f"wrote {diff_out}") + print(f"wrote {tot_out}; sigma(100 GeV)={sig[i100]:.3e} cm^2 " + f"charm_fraction={sig[i100] / (0.677e-38 * 100):.4f}") + + +if __name__ == "__main__": + main() diff --git a/resources/examples/example1/generate_charm_slowrescaling_splines.cpp b/resources/examples/example1/generate_charm_slowrescaling_splines.cpp new file mode 100644 index 000000000..e38ae9bae --- /dev/null +++ b/resources/examples/example1/generate_charm_slowrescaling_splines.cpp @@ -0,0 +1,92 @@ +// Reference generator for the QuarkDISFromSpline slow-rescaling charm-CC splines. +// +// QuarkDISFromSpline samples charm-DIS final states from FITS splines of the +// slow-rescaling differential cross section d2sigma/dxi dy and the total +// sigma(E). The production analysis supplies its own (Dutta-Kim CT14) splines; +// this standalone tool produces physically-validated splines from any installed +// LHAPDF set so the slow-rescaling sampler and normalization can be exercised +// locally (see tests/python/test_quarkdis_slow_rescaling.py). +// +// Physics (leading-order slow rescaling, lab frame, stationary nucleon), exactly +// matching QuarkDISFromSpline's kinematics (slowRescalingQ2 / kinematicallyAllowed): +// xi = struck-quark momentum fraction (the PDF argument) +// Q^2 = 2 M E y xi - m_c^2, W^2 = M^2 + 2 M E y (1-xi) + m_c^2 +// d2sigma/dxi dy = (G_F^2 M E / pi) (M_W^2/(Q^2+M_W^2))^2 +// [ |V_cd|^2 xd(xi,Q^2) + |V_cs|^2 xs(xi,Q^2) +// + (|V_cd|^2 xdbar + |V_cs|^2 xsbar)(1-y)^2 ] +// PDFs are evaluated at xi (NOT at the measured Bjorken x). The integrated charm +// fraction reproduces ~4% at 100 GeV rising to ~6% at 10 TeV (literature band, +// cross-checks the PythiaDISCrossSection charm fraction ~6.5%). +// +// Build/run: +// g++ -std=c++17 $(lhapdf-config --cflags --libs) \ +// generate_charm_slowrescaling_splines.cpp -o gen_sr +// LHAPDF_DATA_PATH= ./gen_sr # writes xidiff.txt + xisigma.txt +// python fit_charm_slowrescaling_splines.py # -> the two FITS splines +#include +#include +#include +#include +#include + +int main(int argc, char** argv) { + std::string pdfname = (argc > 1) ? argv[1] : "CT18NLO"; + LHAPDF::PDF* pdf = LHAPDF::mkPDF(pdfname, 0); + + const double GF = 1.1663787e-5, M = 0.9314943, MW = 80.379; + const double mc = 1.280, MD0 = 1.86484, mmu = 0.105658; + const double Vcd2 = 0.221 * 0.221, Vcs2 = 0.975 * 0.975; + const double GEV2_TO_CM2 = 0.389379e-27; // (hbar c)^2 + + auto dsig = [&](double E, double xi, double y) -> double { + double Q2 = 2 * M * E * y * xi - mc * mc; if (Q2 <= 1.0) return 0.0; + double W2 = M * M + 2 * M * E * y * (1 - xi) + mc * mc; + if (W2 <= (M + MD0) * (M + MD0)) return 0.0; + if (y >= 1 - mmu / E) return 0.0; + double P1 = E, pqx = (mmu * mmu + 2 * P1 * P1 + Q2 + 2 * E * E * (y - 1)) / (2 * P1); + double mq2 = P1 * P1 + Q2 + E * E * (y * y - 1); + if (mq2 - pqx * pqx < 0) return 0.0; + if (xi <= 0 || xi >= 1) return 0.0; + double xd = pdf->xfxQ2(1, xi, Q2), xs = pdf->xfxQ2(3, xi, Q2); + double xdb = pdf->xfxQ2(-1, xi, Q2), xsb = pdf->xfxQ2(-3, xi, Q2); + double pref = GF * GF * M * E / M_PI * std::pow(MW * MW / (Q2 + MW * MW), 2); + double v = pref * ((Vcd2 * xd + Vcs2 * xs) + (Vcd2 * xdb + Vcs2 * xsb) * (1 - y) * (1 - y)); + return v > 0 ? v * GEV2_TO_CM2 : 0.0; // cm^2 + }; + + int nE = 20, nxi = 26, ny = 26; + std::vector Ev(nE), lxi(nxi), ly(ny); + for (int i = 0; i < nE; i++) Ev[i] = std::pow(10, 1.0 + 5.0 * i / (nE - 1)); // 10 GeV - 1 PeV + for (int i = 0; i < nxi; i++) lxi[i] = -4.0 + (std::log10(0.95) + 4.0) * i / (nxi - 1); + for (int i = 0; i < ny; i++) ly[i] = -3.0 + (std::log10(0.95) + 3.0) * i / (ny - 1); + + FILE* fd = std::fopen("xidiff.txt", "w"); + std::fprintf(fd, "%d %d %d\n", nE, nxi, ny); + for (double v : Ev) std::fprintf(fd, "%.10e\n", v); + for (double v : lxi) std::fprintf(fd, "%.10e\n", v); + for (double v : ly) std::fprintf(fd, "%.10e\n", v); + for (int i = 0; i < nE; i++) + for (int j = 0; j < nxi; j++) + for (int k = 0; k < ny; k++) { + double v = dsig(Ev[i], std::pow(10, lxi[j]), std::pow(10, ly[k])); + if (v > 0) std::fprintf(fd, "%.8e 1\n", std::log10(v)); + else std::fprintf(fd, "-99 0\n"); + } + std::fclose(fd); + + FILE* fs = std::fopen("xisigma.txt", "w"); + for (double E : Ev) { + int Nx = 400, Ny = 400; double lo = -4, hi = std::log10(0.95), tot = 0; + double ymax = 1 - mmu / E - 1e-3, dy = (ymax - 1e-3) / Ny; + for (int a = 0; a < Nx; a++) { + double l0 = lo + (hi - lo) * a / Nx, l1 = lo + (hi - lo) * (a + 1) / Nx; + double xi = std::pow(10, 0.5 * (l0 + l1)), dxi = std::pow(10, l1) - std::pow(10, l0), col = 0; + for (int b = 0; b < Ny; b++) { double y = 1e-3 + (b + 0.5) * dy; col += dsig(E, xi, y) * dy; } + tot += col * dxi; + } + std::fprintf(fs, "%.10e %.10e\n", E, tot); + } + std::fclose(fs); + std::printf("wrote xidiff.txt (%dx%dx%d) + xisigma.txt with PDF %s\n", nE, nxi, ny, pdfname.c_str()); + return 0; +} diff --git a/resources/examples/example1/reproduce_dimuon_kinematics.py b/resources/examples/example1/reproduce_dimuon_kinematics.py new file mode 100644 index 000000000..cab6cf96e --- /dev/null +++ b/resources/examples/example1/reproduce_dimuon_kinematics.py @@ -0,0 +1,134 @@ +"""Reproduce the charm dimuon kinematics from the IceCube tau/charm update slides +with the full SIREN+Pythia chain (PythiaDISCrossSection -> CharmMesonDecay). + +For E_nu = 100 GeV nu_mu charged-current charm DIS: PythiaDISCrossSection samples +the primary muon + D meson, then the D decays semileptonically to a muon +(CharmMesonDecay, muon channel). This is the 'pythiaSIREN' curve in the +Pythia-vs-pythiaSIREN comparison (Diffuse WG). Saves the five slide observables +to an .npz for plotting against the slide histograms: + + th_mumu : opening angle (primary mu, decay mu) [deg] + zmu : semileptonic D decay energy fraction E_mu/E_D + y : Bjorken-y = 1 - E_mu,prim / E_nu + th_Dmu : opening angle (D meson, primary mu) [deg] + Q2 : Q^2 = 2 E_nu E_mu,prim (1 - cos th_nu,mu) [GeV^2] + +Requires a Pythia8 build + LHAPDF + a total charm spline (SIREN_PYTHIA_WIDE_SIGMA; +see generate_charm_pythia_splines.py and README_charm.md). The means reproduce +the slide panels (theta_mumu ~9 deg core, ~0.25, ~0.56, theta_Dmu ~7 deg, + ~15 GeV^2). +""" +import os +import math +import sys + +import numpy as np + +import siren +import siren.interactions +import siren.dataclasses +import siren.utilities + +PT = siren.dataclasses.Particle.ParticleType +M_N = 0.9314943 +E_NU = 100.0 + + +def _vmag(p): + return math.sqrt(p[1] ** 2 + p[2] ** 2 + p[3] ** 2) + + +def _angle_deg(a, b): + na, nb = _vmag(a), _vmag(b) + if na == 0 or nb == 0: + return float("nan") + c = (a[1] * b[1] + a[2] * b[2] + a[3] * b[3]) / (na * nb) + return math.degrees(math.acos(max(-1.0, min(1.0, c)))) + + +def main(): + out = sys.argv[1] if len(sys.argv) > 1 else "siren_dimuon.npz" + N = int(sys.argv[2]) if len(sys.argv) > 2 else 2500 + sigma_spline = os.environ["SIREN_PYTHIA_WIDE_SIGMA"] + pdf = os.environ.get("SIREN_PYTHIA_PDF", "LHAPDF6:CT18NLO") + pythia_data = os.environ.get("PYTHIA8DATA", "") + + rng = siren.utilities.SIREN_random(20260506) + xs = siren.interactions.PythiaDISCrossSection( + "", sigma_spline, 1, M_N, 1.0, [PT.NuMu], [PT.PPlus], pythia_data, pdf, "cm") + sigs = list(xs.GetPossibleSignaturesFromParents(PT.NuMu, PT.PPlus)) + + th_mumu, zmu, yv, th_Dmu, Q2v = [], [], [], [], [] + decays = {} + n_done = attempts = 0 + while n_done < N and attempts < N * 30: + attempts += 1 + ir = siren.dataclasses.InteractionRecord() + ir.signature = sigs[n_done % len(sigs)] + ir.primary_momentum = [E_NU, 0.0, 0.0, E_NU] + ir.primary_mass = 0.0 + ir.target_mass = M_N + cdr = siren.dataclasses.CrossSectionDistributionRecord(ir) + try: + xs.SampleFinalState(cdr, rng) + except RuntimeError: + continue + ir_out = siren.dataclasses.InteractionRecord() + ir_out.signature = ir.signature + ir_out.primary_momentum = [E_NU, 0.0, 0.0, E_NU] + ir_out.primary_mass = 0.0 + cdr.finalize(ir_out) + secs = list(ir_out.secondary_momenta) + smass = list(ir_out.secondary_masses) + p_lep, p_D = secs[0], secs[2] # primary muon, D meson + D_type = ir_out.signature.secondary_types[2] + E_lep, E_D = p_lep[0], p_D[0] + if E_D <= 0: + continue + + if D_type not in decays: + try: + decays[D_type] = siren.interactions.CharmMesonDecay(primary_type=D_type) + except Exception: + decays[D_type] = None + dec = decays[D_type] + if dec is None: + continue + mu_sig = None + for ds in dec.GetPossibleSignaturesFromParent(D_type): + st = list(ds.secondary_types) + if len(st) == 3 and st[1] in (PT.MuPlus, PT.MuMinus): + mu_sig = ds + break + if mu_sig is None: + continue + drec = siren.dataclasses.InteractionRecord() + drec.signature = mu_sig + drec.primary_momentum = [p_D[0], p_D[1], p_D[2], p_D[3]] + drec.primary_mass = smass[2] + dcdr = siren.dataclasses.CrossSectionDistributionRecord(drec) + try: + dec.SampleFinalState(dcdr, rng) + except RuntimeError: + continue + dout = siren.dataclasses.InteractionRecord() + dout.signature = mu_sig + dout.primary_momentum = [p_D[0], p_D[1], p_D[2], p_D[3]] + dout.primary_mass = smass[2] + dcdr.finalize(dout) + p_mu_dec = list(dout.secondary_momenta)[1] # decay muon + + th_mumu.append(_angle_deg(p_lep, p_mu_dec)) + zmu.append(p_mu_dec[0] / E_D) + yv.append(1.0 - E_lep / E_NU) + th_Dmu.append(_angle_deg(p_D, p_lep)) + Q2v.append(2.0 * E_NU * E_lep * (1.0 - p_lep[3] / _vmag(p_lep))) + n_done += 1 + + np.savez(out, th_mumu=np.array(th_mumu), zmu=np.array(zmu), y=np.array(yv), + th_Dmu=np.array(th_Dmu), Q2=np.array(Q2v)) + print(f"wrote {out} with {n_done} events") + + +if __name__ == "__main__": + main() From 516d890092e925064eecd340245a8f6bb19e2bc8 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sun, 28 Jun 2026 09:09:01 -0700 Subject: [PATCH 88/93] Fix injector/weighter serialization API in SIREN_Controller and Weighter SetInjectionProcesses/SetPhysicalProcesses assign the distributions list instead of the removed Add*Distribution methods, the initializers construct the pybind _Injector/_Weighter, and SaveEvents uses SaveInjector/SaveWeighter. Weighter.load() restores the weighter through the C++ (injectors, filename) constructor without requiring the detector and processes to be configured first. Add TestSerialization round-trips. --- python/SIREN_Controller.py | 84 ++++++++++++++----------------- python/Weighter.py | 19 +++++-- tests/python/test_controller.py | 89 +++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 51 deletions(-) diff --git a/python/SIREN_Controller.py b/python/SIREN_Controller.py index 844b630eb..146257d74 100644 --- a/python/SIREN_Controller.py +++ b/python/SIREN_Controller.py @@ -116,39 +116,32 @@ def SetInjectionProcesses( # Define the primary injection process primary type self.primary_injection_process.primary_type = primary_type - # Default injection distributions + # Default injection distributions. The pybind Process exposes a + # `distributions` list property (the old Add*Distribution methods were + # removed upstream), so assemble the full list and assign it once. + primary_idist_list = [] if "mass" not in primary_injection_distributions.keys(): - self.primary_injection_process.AddPrimaryInjectionDistribution( - _distributions.PrimaryMass(0) - ) - + primary_idist_list.append(_distributions.PrimaryMass(0)) if "helicity" not in primary_injection_distributions.keys(): - self.primary_injection_process.AddPrimaryInjectionDistribution( - _distributions.PrimaryNeutrinoHelicityDistribution() - ) + primary_idist_list.append(_distributions.PrimaryNeutrinoHelicityDistribution()) # Add all injection distributions for _, idist in primary_injection_distributions.items(): - self.primary_injection_process.AddPrimaryInjectionDistribution(idist) + primary_idist_list.append(idist) + self.primary_injection_process.distributions = primary_idist_list # Loop through possible secondary interactions for i_sec, secondary_type in enumerate(secondary_types): secondary_injection_process = _injection.SecondaryInjectionProcess() secondary_injection_process.primary_type = secondary_type - # Add all injection distributions - for idist in secondary_injection_distributions[i_sec]: - secondary_injection_process.AddSecondaryInjectionDistribution(idist) - + sec_idist_list = list(secondary_injection_distributions[i_sec]) # Add the position distribution if fid_vol_secondary and self.fid_vol is not None: - secondary_injection_process.AddSecondaryInjectionDistribution( - _distributions.SecondaryBoundedVertexDistribution(self.fid_vol) - ) + sec_idist_list.append(_distributions.SecondaryBoundedVertexDistribution(self.fid_vol)) else: - secondary_injection_process.AddSecondaryInjectionDistribution( - _distributions.SecondaryPhysicalVertexDistribution() - ) + sec_idist_list.append(_distributions.SecondaryPhysicalVertexDistribution()) + secondary_injection_process.distributions = sec_idist_list self.secondary_injection_processes.append(secondary_injection_process) @@ -170,30 +163,24 @@ def SetPhysicalProcesses( # Define the primary physical process primary type self.primary_physical_process.primary_type = primary_type - # Default physical distributions + # Default physical distributions (assign the `distributions` list, as for + # the injection processes above). + primary_pdist_list = [] if "mass" not in primary_physical_distributions.keys(): - self.primary_physical_process.AddPhysicalDistribution( - _distributions.PrimaryMass(0) - ) - + primary_pdist_list.append(_distributions.PrimaryMass(0)) if "helicity" not in primary_physical_distributions.keys(): - self.primary_physical_process.AddPhysicalDistribution( - _distributions.PrimaryNeutrinoHelicityDistribution() - ) + primary_pdist_list.append(_distributions.PrimaryNeutrinoHelicityDistribution()) # Add all physical distributions for _, pdist in primary_physical_distributions.items(): - self.primary_physical_process.AddPhysicalDistribution(pdist) + primary_pdist_list.append(pdist) + self.primary_physical_process.distributions = primary_pdist_list # Loop through possible secondary interactions for i_sec, secondary_type in enumerate(secondary_types): secondary_physical_process = _injection.PhysicalProcess() secondary_physical_process.primary_type = secondary_type - - # Add all physical distributions - for pdist in secondary_physical_distributions[i_sec]: - secondary_physical_process.AddPhysicalDistribution(pdist) - + secondary_physical_process.distributions = list(secondary_physical_distributions[i_sec]) self.secondary_physical_processes.append(secondary_physical_process) def SetProcesses( @@ -295,15 +282,14 @@ def InputDarkNewsModel(self, primary_type, table_dir, upscattering=True, decay=T secondary_injection_process = _injection.SecondaryInjectionProcess() secondary_injection_process.primary_type = secondary_type - # Add the secondary position distribution + # Add the secondary position distribution (append to whatever the + # process already carries; the pybind `distributions` is a list property). + sec_dists = list(secondary_injection_process.distributions) if fid_vol_secondary and self.fid_vol is not None: - secondary_injection_process.AddSecondaryInjectionDistribution( - _distributions.SecondaryBoundedVertexDistribution(self.fid_vol) - ) + sec_dists.append(_distributions.SecondaryBoundedVertexDistribution(self.fid_vol)) else: - secondary_injection_process.AddSecondaryInjectionDistribution( - _distributions.SecondaryPhysicalVertexDistribution() - ) + sec_dists.append(_distributions.SecondaryPhysicalVertexDistribution()) + secondary_injection_process.distributions = sec_dists if not inj_sec_defined: self.secondary_injection_processes.append(secondary_injection_process) @@ -510,7 +496,7 @@ def InitializeInjector(self, filenames=None): assert(self.primary_injection_process.primary_type is not None) # Use controller injection objects self.injectors.append( - _injection.Injector( + _injection._Injector( self.events_to_inject, self.detector_model, self.primary_injection_process, @@ -523,7 +509,7 @@ def InitializeInjector(self, filenames=None): assert(len(filenames)>0) # require at least one injector filename for filename in filenames: self.injectors.append( - _injection.Injector( + _injection._Injector( self.events_to_inject, filename, self.random, @@ -537,7 +523,7 @@ def InitializeWeighter(self,filename=None): if filename is None: assert(self.primary_physical_process.primary_type is not None) # Use controller physical objects - self.weighter = _injection.Weighter( + self.weighter = _injection._Weighter( self.injectors, self.detector_model, self.primary_physical_process, @@ -545,7 +531,7 @@ def InitializeWeighter(self,filename=None): ) else: # Try initilalizing with the provided filename - self.weighter = _injection.Weighter( + self.weighter = _injection._Weighter( self.injectors, filename ) @@ -690,9 +676,13 @@ def SaveEvents(self, filename, fill_tables_at_exit=True, datasets["num_interactions"].append(id+1) # save injector and weighter (writes .siren_injector and - # .siren_weighter alongside the event file) - self.injector.save(filename + ".siren_injector") - self.weighter.save(filename) + # .siren_weighter alongside the event file). These are the + # pybind _Injector/_Weighter objects, so use their C++ serialization + # methods (SaveInjector writes the literal path; SaveWeighter appends + # the .siren_weighter suffix). The weighter is optional. + self.injector.SaveInjector(filename + ".siren_injector") + if hasattr(self, "weighter"): + self.weighter.SaveWeighter(filename) # save events ak_array = ak.Array(datasets) diff --git a/python/Weighter.py b/python/Weighter.py index aee604d37..35264098a 100644 --- a/python/Weighter.py +++ b/python/Weighter.py @@ -322,11 +322,22 @@ def save(self, filename: str): def load(self, filename: str): """ - Restore the weighter state from ``.siren_weighter``. + Restore the weighter from ``.siren_weighter``. + + Constructs the underlying C++ weighter via its ``(injectors, filename)`` + constructor, which loads the detector model and physical processes from + the file. The detector / interactions / distributions therefore do NOT + need to be configured first (unlike a freshly built weighter). Only the + injectors are used -- to bind the weighter to your live injection + processes for the generation-probability cancellation; if they were not + set, the injectors serialized in the file are used instead. Args: filename: Base path; the ".siren_weighter" suffix is added. """ - if self.__weighter is None: - self.__initialize_weighter() - self.__weighter.LoadWeighter(filename) + if self.__injectors is not None: + injectors = [injector._Injector__injector if isinstance(injector, _PyInjector) else injector + for injector in self.__injectors] + else: + injectors = [] + self.__weighter = _Weighter(injectors, filename) diff --git a/tests/python/test_controller.py b/tests/python/test_controller.py index 0c37e5b4f..91928a56d 100644 --- a/tests/python/test_controller.py +++ b/tests/python/test_controller.py @@ -482,3 +482,92 @@ def test_multiple_events_give_different_weights(self, full_setup): weighter, events = full_setup weights = [weighter.EventWeight(e) for e in events] assert len(set(weights)) > 1 or len(events) == 1 + + +# --------------------------------------------------------------------------- +# Serialization: injector/weighter save + load round-trips +# --------------------------------------------------------------------------- + +class TestSerialization: + """Guard the injector/weighter serialization the SaveEvents path and the + Weighter wrapper expose. The controller inject->save path was previously + untested, which let a wrapper/pybind API mismatch (.save vs SaveInjector/ + SaveWeighter, and a removed Add*Distribution API) go unnoticed.""" + + def _ccm_pieces(self): + NuMu = dc.Particle.ParticleType.NuMu + dm = detector.DetectorModel() + det_dir = _util.get_detector_model_path("CCM") + dm.LoadMaterialModel(os.path.join(det_dir, "materials.dat")) + dm.LoadDetectorModel(os.path.join(det_dir, "densities.dat")) + int_col = interactions.InteractionCollection(NuMu, [interactions.DummyCrossSection()]) + p_inj = injection.PrimaryInjectionProcess() + p_inj.primary_type = NuMu + p_inj.interactions = int_col + p_inj.distributions = [ + distributions.PrimaryMass(0), distributions.Monoenergetic(1.0), + distributions.IsotropicDirection(), + distributions.PointSourcePositionDistribution(smath.Vector3D(0, 0, 0), 25.0)] + p_phys = injection.PhysicalProcess() + p_phys.primary_type = NuMu + p_phys.interactions = int_col + p_phys.distributions = [distributions.PrimaryMass(0), distributions.IsotropicDirection()] + return NuMu, dm, p_inj, p_phys + + def test_controller_save_events_writes_loadable_injector_and_weighter(self, tmp_path): + """SIREN_Controller inject -> SaveEvents writes .siren_injector / + .siren_weighter via the pybind SaveInjector/SaveWeighter, and the saved + weighter reloads through the (injectors, filename) constructor.""" + from siren.SIREN_Controller import SIREN_Controller + NuMu = dc.Particle.ParticleType.NuMu + try: + c = SIREN_Controller(3, experiment="CCM") + except (AttributeError, TypeError, OSError) as e: + pytest.skip(f"Cannot create CCM controller: {e}") + c.SetInteractions(interactions.InteractionCollection(NuMu, [interactions.DummyCrossSection()])) + inj_d = {"mass": distributions.PrimaryMass(0), "energy": distributions.Monoenergetic(1.0), + "direction": distributions.IsotropicDirection(), + "position": distributions.PointSourcePositionDistribution(smath.Vector3D(0, 0, 0), 25.0)} + phys_d = {"mass": distributions.PrimaryMass(0), "direction": distributions.IsotropicDirection()} + c.SetProcesses(NuMu, inj_d, phys_d) + c.Initialize() + c.GenerateEvents(verbose=False) + base = str(tmp_path / "ev") + c.SaveEvents(base, hdf5=False, parquet=False, verbose=False) + assert os.path.exists(base + ".siren_injector") + assert os.path.exists(base + ".siren_weighter") + w2 = injection._Weighter(c.injectors, base) # (injectors, filename) load ctor + assert np.isfinite(w2.EventWeight(c.events[0])) + + def test_weighter_wrapper_load_needs_only_injectors(self, tmp_path): + """Weighter.load() restores via the (injectors, filename) ctor and must NOT + require the detector / interactions / distributions to be configured.""" + NuMu, dm, p_inj, p_phys = self._ccm_pieces() + inj = injection._Injector(10, dm, p_inj, utilities.SIREN_random(7)) + event = inj.GenerateEvent() + + configured = injection.Weighter( + injectors=[inj], detector_model=dm, primary_type=NuMu, + primary_interactions=[interactions.DummyCrossSection()], + primary_physical_distributions=[distributions.PrimaryMass(0), + distributions.IsotropicDirection()]) + base = str(tmp_path / "w") + configured.save(base) + ref = configured(event) + + # a fresh wrapper that ONLY knows the injectors -- no detector/process config + fresh = injection.Weighter(injectors=[inj]) + fresh.load(base) + assert np.isclose(fresh(event), ref) + + def test_injector_save_reload_roundtrip(self, tmp_path): + """Injector SaveInjector writes the literal path; reload via the filename ctor.""" + NuMu, dm, p_inj, p_phys = self._ccm_pieces() + inj = injection._Injector(10, dm, p_inj, utilities.SIREN_random(11)) + path = str(tmp_path / "inj.siren_injector") + inj.SaveInjector(path) + assert os.path.getsize(path) > 0 + inj2 = injection._Injector(10, path, utilities.SIREN_random(11)) # filename ctor + ev = inj2.GenerateEvent() + w = injection._Weighter([inj2], dm, p_phys) + assert np.isfinite(w.EventWeight(ev)) From 4705b58b55cda28a4b4868588b45bd5d1e695a7d Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sun, 28 Jun 2026 13:04:42 -0700 Subject: [PATCH 89/93] CharmSerialization_TEST: guard under cibuildwheel so wheel builds configure Under cibuildwheel package_add_test is a no-op, so the unconditional target_link_libraries referenced a target that was never created and failed configuration. Wrap the test in if(NOT CIBUILDWHEEL); the plain variable form avoids the empty-string if(NOT ${CIBUILDWHEEL}) that would also drop it from local ctest. --- projects/injection/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/projects/injection/CMakeLists.txt b/projects/injection/CMakeLists.txt index 289be1c16..1d96cb8de 100644 --- a/projects/injection/CMakeLists.txt +++ b/projects/injection/CMakeLists.txt @@ -40,10 +40,16 @@ install(DIRECTORY "${PROJECT_SOURCE_DIR}/projects/injection/public/" #package_add_test(UnitTest_Injector ${PROJECT_SOURCE_DIR}/projects/injection/private/test/Injector_TEST.cxx) package_add_test(UnitTest_Weighter ${PROJECT_SOURCE_DIR}/projects/injection/private/test/Weighter_TEST.cxx) +# Skip under cibuildwheel: package_add_test is a no-op there, so the following +# target_link_libraries would reference a target that was never created. +# Use plain `NOT CIBUILDWHEEL` (not `NOT ${CIBUILDWHEEL}`) so the empty-string +# local case evaluates true and the test still builds/runs in local ctest. +if(NOT CIBUILDWHEEL) package_add_test(UnitTest_CharmSerialization ${PROJECT_SOURCE_DIR}/projects/injection/private/test/CharmSerialization_TEST.cxx) # The injection headers pull in Pybind11Trampoline.h, so this TU needs the # pybind11 headers (header-only; no Python embedding required). target_link_libraries(UnitTest_CharmSerialization pybind11::headers) +endif() if(NOT ${CIBUILDWHEEL}) package_add_test(UnitTest_CCM_HNL ${PROJECT_SOURCE_DIR}/projects/injection/private/test/CCM_HNL_TEST.cxx) target_link_libraries(UnitTest_CCM_HNL pybind11::embed) From cb8c0741a0302bbfa0f682c5985058277c3d5189 Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sun, 28 Jun 2026 18:17:26 -0700 Subject: [PATCH 90/93] charm-DIS: remove unused code paths and dead members Remove the inverse-CDF machinery (computeDiffGammaCDF, inverseCdf) in both CharmMesonDecay classes and the form-factor differential-width path it fed (the four-argument DifferentialDecayWidth and FormFactorFromRecord); the virtual override returns TotalDecayWidthForFinalState. Also drop the commented-out hadronization block, empty if() blocks and duplicate includes in QuarkDISFromSpline, the unused GetHadronMass and pythia_initialized_ flag, and the unused targets_by_primary_types_ and D_types_ members. --- .../interactions/private/CharmMesonDecay.cxx | 189 +----------------- .../private/CharmMesonDecay3Body.cxx | 162 +-------------- projects/interactions/private/DMesonELoss.cxx | 2 - .../private/PythiaDISCrossSection.cxx | 22 +- .../private/QuarkDISFromSpline.cxx | 109 ---------- .../private/test/CharmMesonDecay_TEST.cxx | 92 +-------- .../SIREN/interactions/CharmMesonDecay.h | 4 - .../SIREN/interactions/CharmMesonDecay3Body.h | 4 - .../interactions/PythiaDISCrossSection.h | 4 - .../SIREN/interactions/QuarkDISFromSpline.h | 2 - 10 files changed, 13 insertions(+), 577 deletions(-) diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx index 9a67c6142..be4cedce6 100644 --- a/projects/interactions/private/CharmMesonDecay.cxx +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -21,65 +21,9 @@ namespace siren { namespace interactions { -CharmMesonDecay::CharmMesonDecay() { - // this is the default initialization but should never be used - - // we need to compute cdf here b/c otherwise SampleFinalState becomes non constant - // in this case, we will need to compute all the possible dGamma's here - // maybe add a map here, but for now we hard code first - std::vector constants; - constants.resize(3); - // check the primary and secondaries of the signature - constants[0] = 0.725; // this is f^+(0)|V_cs| for charged D - constants[1] = 0.44; // this is alpha, same for all K final states - constants[2] = 2.01027; // this is excited charged D meson - - double mD = particleMass(siren::dataclasses::Particle::ParticleType::DPlus); - double mK = particleMass(siren::dataclasses::Particle::ParticleType::K0Bar); - - computeDiffGammaCDF(constants, mD, mK); -} - -CharmMesonDecay::CharmMesonDecay(siren::dataclasses::Particle::ParticleType primary) { - - //standard stuff, constant across primary types - std::vector constants; - constants.resize(3); - double mD; - double mK; - - // Form-factor constants are CP-symmetric (|V_cs|, alpha, m_D*); the cached - // inverseCdf table is dead code in current SIREN, so anti-flavor instances - // share the same body as their particle counterparts. - if (primary == siren::dataclasses::Particle::ParticleType::DPlus || - primary == siren::dataclasses::Particle::ParticleType::DMinus) { - constants[0] = 0.725; // this is f^+(0)|V_cs| for charged D - constants[1] = 0.44; // this is alpha, same for all K final states - constants[2] = 2.01027; // this is excited charged D meson - - mD = particleMass(siren::dataclasses::Particle::ParticleType::DPlus); - mK = particleMass(siren::dataclasses::Particle::ParticleType::K0Bar); - - } else if (primary == siren::dataclasses::Particle::ParticleType::D0 || - primary == siren::dataclasses::Particle::ParticleType::D0Bar) { - constants[0] = 0.719; // this is f^+(0)|V_cs| for charged D - constants[1] = 0.50; // this is alpha, same for all K final states - constants[2] = 2.00697; // this is excited charged D meson - - mD = particleMass(siren::dataclasses::Particle::ParticleType::D0); - mK = particleMass(siren::dataclasses::Particle::ParticleType::KMinus); - } else if (primary == siren::dataclasses::Particle::ParticleType::DsPlus || - primary == siren::dataclasses::Particle::ParticleType::DsMinus) { - // Ds -> (eta / eta' / phi) + mu + nu uses pure 3-body phase space (no - // form factor). Daughter is sampled inline in SampleFinalState. The - // computeDiffGammaCDF table is only consumed by D+/D0 form-factor logic, - // so skip it here. - return; - } +CharmMesonDecay::CharmMesonDecay() {} - computeDiffGammaCDF(constants, mD, mK); - -} +CharmMesonDecay::CharmMesonDecay(siren::dataclasses::Particle::ParticleType primary) {} bool CharmMesonDecay::equal(Decay const & other) const { const CharmMesonDecay* x = dynamic_cast(&other); @@ -361,135 +305,8 @@ std::vector CharmMesonDecay::GetPossibleSigna return signatures; } -std::vector CharmMesonDecay::FormFactorFromRecord(dataclasses::CrossSectionDistributionRecord const & record) const { - dataclasses::InteractionSignature signature = record.signature; - std::vector constants; - constants.resize(3); - // check the primary and secondaries of the signature - // Form-factor constants are CP-symmetric -- anti-flavor cases mirror the c cases. - if ((signature.primary_type == dataclasses::Particle::ParticleType::DPlus && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::K0Bar) || - (signature.primary_type == dataclasses::Particle::ParticleType::DMinus && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::K0)) { - constants[0] = 0.725; // this is f^+(0)|V_cs| for charged D - constants[1] = 0.44; // this is alpha, same for all K final states - constants[2] = 2.01027; // this is excited charged D meson - } else if ((signature.primary_type == dataclasses::Particle::ParticleType::D0 && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::KMinus) || - (signature.primary_type == dataclasses::Particle::ParticleType::D0Bar && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::KPlus)) { - constants[0] = 0.719; // this is f^+(0)|V_cs| for neutral D - constants[1] = 0.50; // this is alpha, same for all K final states - constants[2] = 2.00697; // this is excited neutral D meson - } - return constants; -} - double CharmMesonDecay::DifferentialDecayWidth(dataclasses::InteractionRecord const & record) const { - // first let the fully hadronic state be handled separately. Use size==1 so - // we don't mis-route the Ds semileptonic mode (Hadrons + mu + nu, size 3). - dataclasses::InteractionSignature signature = record.signature; - if (signature.secondary_types.size() == 1 && - signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::Hadrons) { - return TotalDecayWidthForFinalState(record); - } - // Ds semileptonic uses pure phase space (no form factor); FinalStateProbability - // for Ds is dd/td which is handled by the matrix-element-free flat sampling. - // Returning the total width here makes FinalStateProbability = 1, which is the - // right thing when the kinematic distribution is sampled directly from phase - // space (no reweighting needed). - if (signature.primary_type == siren::dataclasses::Particle::ParticleType::DsPlus || - signature.primary_type == siren::dataclasses::Particle::ParticleType::DsMinus) { - return TotalDecayWidthForFinalState(record); - } - // get the form factor constants - std::vector constants = FormFactorFromRecord(record); - // calculate the q^2 - rk::P4 pD(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); - rk::P4 pKPi(geom3::Vector3(record.secondary_momenta[0][1], record.secondary_momenta[0][2], record.secondary_momenta[0][3]), record.secondary_masses[0]); - double Q2 = (pD - pKPi).dot(pD - pKPi); - // primary and secondary masses are also needed - double mD = record.primary_mass; - double mK = record.secondary_masses[0]; - return DifferentialDecayWidth(constants, Q2, mD, mK); -} - -double CharmMesonDecay::DifferentialDecayWidth(std::vector constants, double Q2, double mD, double mK) const { - // get the numerical constants from the vector - double F0CKM = constants[0]; - double alpha = constants[1]; - double ms = constants[2]; - double Q2tilde = Q2 / (ms * ms); - // compute the 3-momentum as a function of Q2 - // double EK = 0.5 * (Q2 - pow(mD, 2) + pow(mK, 2)) / mD; // energy of Kaon - double EK = 0.5 * (pow(mD, 2) + pow(mK, 2) - Q2) / mD; // energy of Kaon - if (EK * EK < mK * mK) return 0.0; - - double PK = pow(pow(EK, 2) - pow(mK, 2), 0.5); - // plug in the constants - double dGamma = pow(siren::utilities::Constants::FermiConstant,2) / (24 * pow(siren::utilities::Constants::pi,3)) * pow(F0CKM,2) * - pow((1/((1-Q2tilde) * (1 - alpha * Q2tilde))),2) * pow(PK,3); - return dGamma; -} - -void CharmMesonDecay::computeDiffGammaCDF(std::vector constants, double mD, double mK) { - - // returns a 1D interpolater table for dGamma cdf - // define the pdf with only Q2 as the input - std::function pdf = [&] (double x) -> double { - return DifferentialDecayWidth(constants, x, mD, mK); - }; - // first normalize the integral - double min = 0; - double max = 1.4; // these set the min and max of the Q2 considered - double normalization = siren::utilities::rombergIntegrate(pdf, min, max); - std::function normed_pdf = [&] (double x) -> double { - return DifferentialDecayWidth(constants, x, mD, mK) / normalization; - }; - // now create the spline and compute the CDF - - // set the Q2 nodes (use 100 nodes) - std::vector Q2spline; - for (int i = 0; i < 100; ++i) { - Q2spline.push_back(0.01 + i * (max-min) / 100 ); - } - - // declare the cdf vectors - std::vector cdf_vector; - std::vector cdf_Q2_nodes; - std::vector pdf_vector; - - cdf_Q2_nodes.push_back(0); - cdf_vector.push_back(0); - pdf_vector.push_back(0); - - // compute the spline table - for (int i = 0; i < Q2spline.size(); ++i) { - if (i == 0) { - double cur_Q2 = Q2spline[i]; - double cur_pdf = normed_pdf(cur_Q2); - double area = cur_Q2 * cur_pdf * 0.5; - pdf_vector.push_back(cur_pdf); - cdf_vector.push_back(area); - cdf_Q2_nodes.push_back(cur_Q2); - continue; - } - double cur_Q2 = Q2spline[i]; - double cur_pdf = normed_pdf(cur_Q2); - double area = 0.5 * (pdf_vector[i - 1] + cur_pdf) * (Q2spline[i] - Q2spline[i - 1]); - pdf_vector.push_back(cur_pdf); - cdf_Q2_nodes.push_back(cur_Q2); - cdf_vector.push_back(area + cdf_vector.back()); - } - - cdf_Q2_nodes.push_back(max); - cdf_vector.push_back(1); - pdf_vector.push_back(0); - - // set the spline table - siren::utilities::TableData1D inverse_cdf_data; - inverse_cdf_data.x = cdf_vector; - inverse_cdf_data.f = cdf_Q2_nodes; - - inverseCdf = siren::utilities::Interpolator1D(inverse_cdf_data); - return; - + return TotalDecayWidthForFinalState(record); } // this is temporary implementation diff --git a/projects/interactions/private/CharmMesonDecay3Body.cxx b/projects/interactions/private/CharmMesonDecay3Body.cxx index fe701916e..d58ad2f6b 100644 --- a/projects/interactions/private/CharmMesonDecay3Body.cxx +++ b/projects/interactions/private/CharmMesonDecay3Body.cxx @@ -21,54 +21,13 @@ namespace siren { namespace interactions { -CharmMesonDecay3Body::CharmMesonDecay3Body() { - // this is the default initialization but should never be used - - // we need to compute cdf here b/c otherwise SampleFinalState becomes non constant - // in this case, we will need to compute all the possible dGamma's here - // maybe add a map here, but for now we hard code first - std::vector constants; - constants.resize(3); - // check the primary and secondaries of the signature - constants[0] = 0.725; // this is f^+(0)|V_cs| for charged D - constants[1] = 0.44; // this is alpha, same for all K final states - constants[2] = 2.01027; // this is excited charged D meson - - double mD = particleMass(siren::dataclasses::Particle::ParticleType::DPlus); - double mK = particleMass(siren::dataclasses::Particle::ParticleType::K0Bar); - - computeDiffGammaCDF(constants, mD, mK); -} +CharmMesonDecay3Body::CharmMesonDecay3Body() {} CharmMesonDecay3Body::CharmMesonDecay3Body(siren::dataclasses::Particle::ParticleType primary) { - - //standard stuff, constant across primary types - std::vector constants; - constants.resize(3); - double mD; - double mK; - - if (primary == siren::dataclasses::Particle::ParticleType::DPlus) { - constants[0] = 0.725; // this is f^+(0)|V_cs| for charged D - constants[1] = 0.44; // this is alpha, same for all K final states - constants[2] = 2.01027; // this is excited charged D meson - - mD = particleMass(siren::dataclasses::Particle::ParticleType::DPlus); - mK = particleMass(siren::dataclasses::Particle::ParticleType::K0Bar); - - } else if (primary == siren::dataclasses::Particle::ParticleType::D0) { - constants[0] = 0.719; // this is f^+(0)|V_cs| for charged D - constants[1] = 0.50; // this is alpha, same for all K final states - constants[2] = 2.00697; // this is excited charged D meson - - mD = particleMass(siren::dataclasses::Particle::ParticleType::D0); - mK = particleMass(siren::dataclasses::Particle::ParticleType::KMinus); - } else { + if (primary != siren::dataclasses::Particle::ParticleType::DPlus && + primary != siren::dataclasses::Particle::ParticleType::D0) { throw std::runtime_error("CharmMesonDecay3Body: only D0 and D+ are implemented. Use CharmMesonDecay, which covers D0/D+/Ds and their anti-flavors."); } - - computeDiffGammaCDF(constants, mD, mK); - } bool CharmMesonDecay3Body::equal(Decay const & other) const { @@ -242,121 +201,8 @@ std::vector CharmMesonDecay3Body::GetPossible return signatures; } -std::vector CharmMesonDecay3Body::FormFactorFromRecord(dataclasses::CrossSectionDistributionRecord const & record) const { - dataclasses::InteractionSignature signature = record.signature; - std::vector constants; - constants.resize(3); - // check the primary and secondaries of the signature - if (signature.primary_type == dataclasses::Particle::ParticleType::DPlus && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::K0Bar) { - constants[0] = 0.725; // this is f^+(0)|V_cs| for charged D - constants[1] = 0.44; // this is alpha, same for all K final states - constants[2] = 2.01027; // this is excited charged D meson - } else if (signature.primary_type == dataclasses::Particle::ParticleType::D0 && signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::KMinus) { - constants[0] = 0.719; // this is f^+(0)|V_cs| for neutral D - constants[1] = 0.50; // this is alpha, same for all K final states - constants[2] = 2.00697; // this is excited neutral D meson - } - return constants; -} - double CharmMesonDecay3Body::DifferentialDecayWidth(dataclasses::InteractionRecord const & record) const { - // first let the fully hadronic state be handled separately - dataclasses::InteractionSignature signature = record.signature; - if (signature.secondary_types[0] == siren::dataclasses::Particle::ParticleType::Hadrons) { - return TotalDecayWidthForFinalState(record); - } - // get the form factor constants - std::vector constants = FormFactorFromRecord(record); - // calculate the q^2 - rk::P4 pD(geom3::Vector3(record.primary_momentum[1], record.primary_momentum[2], record.primary_momentum[3]), record.primary_mass); - rk::P4 pKPi(geom3::Vector3(record.secondary_momenta[0][1], record.secondary_momenta[0][2], record.secondary_momenta[0][3]), record.secondary_masses[0]); - double Q2 = (pD - pKPi).dot(pD - pKPi); - // primary and secondary masses are also needed - double mD = record.primary_mass; - double mK = record.secondary_masses[0]; - return DifferentialDecayWidth(constants, Q2, mD, mK); -} - -double CharmMesonDecay3Body::DifferentialDecayWidth(std::vector constants, double Q2, double mD, double mK) const { - // get the numerical constants from the vector - double F0CKM = constants[0]; - double alpha = constants[1]; - double ms = constants[2]; - double Q2tilde = Q2 / (ms * ms); - // compute the 3-momentum as a function of Q2 - // double EK = 0.5 * (Q2 - pow(mD, 2) + pow(mK, 2)) / mD; // energy of Kaon - double EK = 0.5 * (pow(mD, 2) + pow(mK, 2) - Q2) / mD; // energy of Kaon - if (EK * EK < mK * mK) return 0.0; - - double PK = pow(pow(EK, 2) - pow(mK, 2), 0.5); - // plug in the constants - double dGamma = pow(siren::utilities::Constants::FermiConstant,2) / (24 * pow(siren::utilities::Constants::pi,3)) * pow(F0CKM,2) * - pow((1/((1-Q2tilde) * (1 - alpha * Q2tilde))),2) * pow(PK,3); - return dGamma; -} - -void CharmMesonDecay3Body::computeDiffGammaCDF(std::vector constants, double mD, double mK) { - - // returns a 1D interpolater table for dGamma cdf - // define the pdf with only Q2 as the input - std::function pdf = [&] (double x) -> double { - return DifferentialDecayWidth(constants, x, mD, mK); - }; - // first normalize the integral - double min = 0; - double max = 1.4; // these set the min and max of the Q2 considered - double normalization = siren::utilities::rombergIntegrate(pdf, min, max); - std::function normed_pdf = [&] (double x) -> double { - return DifferentialDecayWidth(constants, x, mD, mK) / normalization; - }; - // now create the spline and compute the CDF - - // set the Q2 nodes (use 100 nodes) - std::vector Q2spline; - for (int i = 0; i < 100; ++i) { - Q2spline.push_back(0.01 + i * (max-min) / 100 ); - } - - // declare the cdf vectors - std::vector cdf_vector; - std::vector cdf_Q2_nodes; - std::vector pdf_vector; - - cdf_Q2_nodes.push_back(0); - cdf_vector.push_back(0); - pdf_vector.push_back(0); - - // compute the spline table - for (int i = 0; i < Q2spline.size(); ++i) { - if (i == 0) { - double cur_Q2 = Q2spline[i]; - double cur_pdf = normed_pdf(cur_Q2); - double area = cur_Q2 * cur_pdf * 0.5; - pdf_vector.push_back(cur_pdf); - cdf_vector.push_back(area); - cdf_Q2_nodes.push_back(cur_Q2); - continue; - } - double cur_Q2 = Q2spline[i]; - double cur_pdf = normed_pdf(cur_Q2); - double area = 0.5 * (pdf_vector[i - 1] + cur_pdf) * (Q2spline[i] - Q2spline[i - 1]); - pdf_vector.push_back(cur_pdf); - cdf_Q2_nodes.push_back(cur_Q2); - cdf_vector.push_back(area + cdf_vector.back()); - } - - cdf_Q2_nodes.push_back(max); - cdf_vector.push_back(1); - pdf_vector.push_back(0); - - // set the spline table - siren::utilities::TableData1D inverse_cdf_data; - inverse_cdf_data.x = cdf_vector; - inverse_cdf_data.f = cdf_Q2_nodes; - - inverseCdf = siren::utilities::Interpolator1D(inverse_cdf_data); - return; - + return TotalDecayWidthForFinalState(record); } // this is temporary implementation diff --git a/projects/interactions/private/DMesonELoss.cxx b/projects/interactions/private/DMesonELoss.cxx index 1629e1580..6b157ff5c 100644 --- a/projects/interactions/private/DMesonELoss.cxx +++ b/projects/interactions/private/DMesonELoss.cxx @@ -85,8 +85,6 @@ std::vector DMesonELoss::GetPossibleSignature return signatures; } -// i am here - double DMesonELoss::TotalCrossSection(dataclasses::InteractionRecord const & interaction) const { siren::dataclasses::Particle::ParticleType primary_type = interaction.signature.primary_type; rk::P4 p1(geom3::Vector3(interaction.primary_momentum[1], interaction.primary_momentum[2], interaction.primary_momentum[3]), interaction.primary_mass); diff --git a/projects/interactions/private/PythiaDISCrossSection.cxx b/projects/interactions/private/PythiaDISCrossSection.cxx index 7701db20b..6bc806adc 100644 --- a/projects/interactions/private/PythiaDISCrossSection.cxx +++ b/projects/interactions/private/PythiaDISCrossSection.cxx @@ -214,19 +214,6 @@ double PythiaDISCrossSection::GetLeptonMass(siren::dataclasses::ParticleType lep } } -double PythiaDISCrossSection::GetHadronMass(siren::dataclasses::ParticleType hadron_type) { - switch(hadron_type) { - case siren::dataclasses::ParticleType::D0: - case siren::dataclasses::ParticleType::D0Bar: - return siren::utilities::Constants::D0Mass; - case siren::dataclasses::ParticleType::DPlus: - case siren::dataclasses::ParticleType::DMinus: - return siren::utilities::Constants::DPlusMass; - default: - return 0.0; - } -} - std::map PythiaDISCrossSection::getIndices(siren::dataclasses::InteractionSignature signature) { // Identify meson by elimination (not via isD(), which only covers D0/D+/-). // First pass: claim lepton and Hadrons. Second pass: whatever remains is the meson. @@ -307,17 +294,18 @@ void PythiaDISCrossSection::InitializeSignatures() { (primary_type == siren::dataclasses::ParticleType::NuEBar || primary_type == siren::dataclasses::ParticleType::NuMuBar || primary_type == siren::dataclasses::ParticleType::NuTauBar); + std::set D_types_local; if (is_antineutrino) { - D_types_ = {siren::dataclasses::ParticleType::D0Bar, + D_types_local = {siren::dataclasses::ParticleType::D0Bar, siren::dataclasses::ParticleType::DMinus, siren::dataclasses::ParticleType::DsMinus}; } else { - D_types_ = {siren::dataclasses::ParticleType::D0, + D_types_local = {siren::dataclasses::ParticleType::D0, siren::dataclasses::ParticleType::DPlus, siren::dataclasses::ParticleType::DsPlus}; } - for (auto meson_type : D_types_) { + for (auto meson_type : D_types_local) { dataclasses::InteractionSignature full_signature = signature; full_signature.secondary_types.push_back(meson_type); for(auto target_type : target_types_) { @@ -578,8 +566,6 @@ void PythiaDISCrossSection::InitializePythia(double E_nu, int target_pdg) const // The SIREN RNG is connected here and updated per-event in SampleFinalState. siren_rndm_ = std::make_shared(); pythia_->rndm.rndmEnginePtr(siren_rndm_); - - pythia_initialized_ = true; } void PythiaDISCrossSection::GeneratePythiaCharmSamples( diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 751f025f5..5fabdbf10 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -10,18 +10,12 @@ #include // for vector #include // for assert #include // for size_t -#include -#include -#include -#include #include -#include #include // for P4, Boost #include // for Vector3 #include // for splinetable -//#include #include "SIREN/interactions/CrossSection.h" // for CrossSection #include "SIREN/dataclasses/InteractionRecord.h" // for Interactio... @@ -513,8 +507,6 @@ double QuarkDISFromSpline::TotalCrossSection(siren::dataclasses::ParticleType pr total_cross_section_.searchcenters(&log_energy, ¢er); double log_xs = total_cross_section_.ndsplineeval(&log_energy, ¢er, 0); - if (std::pow(10.0, log_xs) == 0) { - } return unit * std::pow(10.0, log_xs); } @@ -609,8 +601,6 @@ double QuarkDISFromSpline::DifferentialCrossSection(double energy, double xi, do } double result = pow(10., differential_cross_section_.ndsplineeval(coordinates.data(), centers.data(), 0)); assert(result >= 0); - if (std::isinf(result)) { - } return unit * result; } @@ -926,105 +916,6 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR hadron.SetMass(p4X_msq > 0.0 ? std::sqrt(p4X_msq) : 0.0); hadron.SetHelicity(record.target_helicity); meson.SetFourMomentum({p4CH.e(), p4CH.px(), p4CH.py(), p4CH.pz()}); - - - // ############################################# - // Included for posterity: original hadronization scheme - // ############################################## - - /* - - rk::P4 p4_lab = p2_lab + pq_lab; - - rk::P4 p3; - rk::P4 p4; - p3 = p3_lab; // now we have our lepton momentum set, which should not be modified from here on - p4 = p4_lab; // momentum of the virtual charm - - - // compute the energy and 3-momentum of the virtual charm - double p3c = std::sqrt(std::pow(p4.px(), 2) + std::pow(p4.py(), 2) + std::pow(p4.pz(), 2)); - double Ec = p4.e(); //energy of primary charm - double mCH = getHadronMass(record.signature.secondary_types[meson_index]); // obtain charmed hadron mass - - // accept-reject sampling for a valid momentum fragmentation - bool frag_accept; - double randValue; - double z; - double ECH; - - // add a maximum number of trials in the while loop - int max_sampling = 500; - int sampling = 0; - - // sample again if this eenrgy is not kinematically allowed - // this samples in the lab frame the energy of the D-meson such that mass is real - do { - sampling += 1; - if (sampling > max_sampling) { - // throw(siren::utilities::InjectionFailure("Failed to sample hadronization!")); - break; - } - randValue = random->Uniform(0,1); - z = inverseCdfTable(randValue); - ECH = z * Ec; - if (std::pow(ECH, 2) - std::pow(mCH, 2) <= 0) { - frag_accept = false; - } else { - frag_accept = true; - } - } while (!frag_accept); - // new attempt of using the isoscalar mass as the remnant hadronic shower mass - double mX = target_mass_; - double Mc = p4.m(); - //compute the energies in the charm rest frame - double E_CH_c = (std::pow(Mc, 2) - std::pow(mX, 2) + std::pow(mCH, 2)) / (2 * Mc); - double p_c = std::sqrt((std::pow(Mc, 2) - std::pow(mCH + mX, 2)) * (std::pow(Mc, 2) - std::pow(mCH - mX, 2))) / (2 * Mc); - // compute the lorentz boost parameters - double gamma = p4.gamma(); - double beta = p4.beta(); - // using the lab frame fragmented energy and the - double cosTheta = std::max(std::min(((ECH - gamma * E_CH_c)/(gamma * beta * p_c)), 1.), -1.); - // now compute the momentum vectors in the rest frame - double sinTheta = std::sin(std::acos(cosTheta)); - rk::P4 p4CH_c(p_c * geom3::Vector3(cosTheta, sinTheta, 0), mCH); - rk::P4 p4X_c(p_c * geom3::Vector3(-cosTheta, -sinTheta, 0), mX); - // these all assume boost direction is charm direction. Now we should rotate back to charm lab momentum direction - geom3::Vector3 pc_lab_momentum = p4.momentum(); - geom3::UnitVector3 pc_lab_dir = pc_lab_momentum.direction(); - geom3::Rotation3 x_to_pc_lab_rot = geom3::rotationBetween(x_dir, pc_lab_dir); - p4X_c.rotate(x_to_pc_lab_rot); - p4CH_c.rotate(x_to_pc_lab_rot); - - // finally, we perform a random azimuthal rotation - double c_phi = random->Uniform(0, 2 * M_PI); - geom3::Rotation3 azimuth_rand_rot(pc_lab_dir, c_phi); - p4X_c.rotate(azimuth_rand_rot); - p4CH_c.rotate(azimuth_rand_rot); - - // and boost them back to the lab frame - rk::Boost boost_from_crest_to_lab = p4.labBoost(); - rk::P4 p4X = p4X_c.boost(boost_from_crest_to_lab); - rk::P4 p4CH = p4CH_c.boost(boost_from_crest_to_lab); - - - - // now we proceed to saving the final state kinematics - std::vector & secondaries = record.GetSecondaryParticleRecords(); - siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[lepton_index]; - siren::dataclasses::SecondaryParticleRecord & hadron = secondaries[hadron_index]; - siren::dataclasses::SecondaryParticleRecord & meson = secondaries[meson_index]; - - lepton.SetFourMomentum({p3.e(), p3.px(), p3.py(), p3.pz()}); - lepton.SetMass(p3.m()); - lepton.SetHelicity(record.primary_helicity); - hadron.SetFourMomentum({p4X.e(), p4X.px(), p4X.py(), p4X.pz()}); - hadron.SetMass(p4X.m()); - hadron.SetHelicity(record.target_helicity); - meson.SetFourMomentum({p4CH.e(), p4CH.px(), p4CH.py(), p4CH.pz()}); - meson.SetMass(p4CH.m()); - meson.SetHelicity(record.target_helicity); // this needs working on - */ } double QuarkDISFromSpline::FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const { diff --git a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx index 3ffc4200f..6b653de55 100644 --- a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx +++ b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx @@ -5,7 +5,6 @@ * Tests: * 1. Interpolator1D with a known linear inverse CDF (sanity check) * 2. Interpolator1D with the actual D meson decay CDF table - * 2b. DifferentialDecayWidth (4-arg + from-record) vs analytic form factor * 3. SampleFinalState q^2 distribution closes with FinalStateProbability * 4. TotalDecayWidthForFinalState throws on unsupported signatures * @@ -83,7 +82,8 @@ TEST(Interpolator1D, LinearInverseCDF) { // --- Test 2: Interpolator1D with D meson decay CDF ----------------------- TEST(Interpolator1D, DMesonDecayCDF) { - // Replicate computeDiffGammaCDF for D0 -> K- e+ nu_e + // Build an inverse-CDF table locally from a single-pole form factor for + // D0 -> K- e+ nu_e (self-contained Interpolator1D sanity check). double mD = Constants::D0Mass; double mK = Constants::KMinusMass; double F0CKM = 0.719; @@ -178,94 +178,6 @@ TEST(Interpolator1D, DMesonDecayCDF) { EXPECT_NEAR(mean_interp, mean_linear, 0.05); } -// --- Test 2b: DifferentialDecayWidth vs analytic form factor ------------- - -TEST(CharmMesonDecay, DiffDecayWidthComparison) { - // Verify the SIREN DifferentialDecayWidth matches the analytic single-pole - // form-factor formula, both via the 4-arg path and the from-record path. - double mD = Constants::D0Mass; // PDG D0 mass - double mK = Constants::KMinusMass; - double F0CKM = 0.719; - double alpha = 0.50; // D0 pole parameter - double ms_star = 2.00697; - double GF = Constants::FermiConstant; - - // Analytic form factor. Note EK enters only as EK*EK, so the sign - // convention of EK is irrelevant (matches the source 4-arg routine). - auto dGamma_analytical = [&](double Q2) -> double { - double Q2tilde = Q2 / (ms_star * ms_star); - double ff2 = std::pow(F0CKM / ((1 - Q2tilde) * (1 - alpha * Q2tilde)), 2); - double EK = 0.5 * (Q2 - (mD * mD + mK * mK)) / mD; - double pk_sq = EK * EK - mK * mK; - if (pk_sq < 0) return 0.0; - return std::pow(GF, 2) / (24 * std::pow(M_PI, 3)) * ff2 * std::pow(std::sqrt(pk_sq), 3); - }; - - CharmMesonDecay decay(ParticleType::D0); - auto sigs = decay.GetPossibleSignaturesFromParent(ParticleType::D0); - auto sig = sigs[0]; // D0 -> K- e+ nu_e - - double Q2_tests[] = {0.01, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0, 1.2, 1.35}; - for (double Q2 : Q2_tests) { - double anal = dGamma_analytical(Q2); - if (anal <= 0.0) continue; - - // from-record path: reconstruct the kinematics in the D rest frame. - double EK_rest = (mD * mD + mK * mK - Q2) / (2 * mD); - double PK_rest = std::sqrt(std::max(0.0, EK_rest * EK_rest - mK * mK)); - - InteractionRecord rec; - rec.signature = sig; - rec.primary_mass = mD; - rec.primary_momentum = {mD, 0, 0, 0}; // D at rest - rec.target_mass = 0; - rec.secondary_momenta = { - {EK_rest, PK_rest, 0, 0}, // K along x - {0, 0, 0, 0}, // lepton (not used by DDW) - {0, 0, 0, 0} // neutrino (not used by DDW) - }; - rec.secondary_masses = {mK, Constants::electronMass, 0.0}; - - double siren_ddw_from_record = decay.DifferentialDecayWidth(rec); - - // 4-arg path with the D0 form-factor constants. - std::vector my_constants = {F0CKM, alpha, ms_star}; - double siren_ddw_4arg = decay.DifferentialDecayWidth(my_constants, Q2, mD, mK); - - // The 4-arg routine reproduces the analytic formula to floating point. - EXPECT_NEAR(siren_ddw_4arg, anal, std::abs(anal) * 1e-9); - // The from-record path round-trips Q^2 through 4-vectors; looser tol. - EXPECT_NEAR(siren_ddw_from_record, anal, std::abs(anal) * 1e-6); - } - - // Pin the per-meson pole parameter alpha used by FormFactorFromRecord: - // 0.50 for D0, 0.44 for D+. - InteractionRecord d0rec; - d0rec.signature = sig; - d0rec.primary_mass = mD; - d0rec.primary_momentum = {mD, 0, 0, 0}; - d0rec.secondary_momenta = {{mK, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; - d0rec.secondary_masses = {mK, Constants::electronMass, 0.0}; - CrossSectionDistributionRecord d0cdr(d0rec); - std::vector d0ff = decay.FormFactorFromRecord(d0cdr); - EXPECT_NEAR(d0ff[1], 0.50, 1e-12); - - CharmMesonDecay decayp(ParticleType::DPlus); - auto sigsp = decayp.GetPossibleSignaturesFromParent(ParticleType::DPlus); - auto sigp = sigsp[0]; // D+ -> K0bar e+ nu - double mDp = Constants::DPlusMass; - double mK0 = Constants::K0Mass; - InteractionRecord dprec; - dprec.signature = sigp; - dprec.primary_mass = mDp; - dprec.primary_momentum = {mDp, 0, 0, 0}; - dprec.secondary_momenta = {{mK0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; - dprec.secondary_masses = {mK0, Constants::electronMass, 0.0}; - CrossSectionDistributionRecord dpcdr(dprec); - std::vector dpff = decayp.FormFactorFromRecord(dpcdr); - EXPECT_NEAR(dpff[1], 0.44, 1e-12); -} - // --- Test 3: SampleFinalState q^2 closes with FinalStateProbability ------- namespace { diff --git a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h index e2f905c6b..ff640fdc9 100644 --- a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h +++ b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h @@ -35,7 +35,6 @@ class CharmMesonDecay : public Decay { friend cereal::access; private: const std::set primary_types = {siren::dataclasses::Particle::ParticleType::D0, siren::dataclasses::Particle::ParticleType::DPlus, siren::dataclasses::Particle::ParticleType::DsPlus, siren::dataclasses::Particle::ParticleType::D0Bar, siren::dataclasses::Particle::ParticleType::DMinus, siren::dataclasses::Particle::ParticleType::DsMinus}; - siren::utilities::Interpolator1D inverseCdf; // for dGamma (form-factor model; not used by FinalStateProbability) // Shared closure helpers: SampleFinalState's density and FinalStateProbability // both build on these, so Sample == Density by construction. static double KStarMass(); @@ -56,14 +55,11 @@ friend cereal::access; double TotalDecayWidth(siren::dataclasses::Particle::ParticleType primary) const override; double TotalDecayWidthForFinalState(dataclasses::InteractionRecord const &) const override; double DifferentialDecayWidth(dataclasses::InteractionRecord const &) const override; - double DifferentialDecayWidth(std::vector constants, double Q2, double mD, double mK) const; void SampleFinalStateHadronic(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr) const; void SampleFinalState(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr) const override; std::vector GetPossibleSignatures() const override; std::vector GetPossibleSignaturesFromParent(siren::dataclasses::Particle::ParticleType primary) const override; virtual double FinalStateProbability(dataclasses::InteractionRecord const & record) const override; - std::vector FormFactorFromRecord(dataclasses::CrossSectionDistributionRecord const & record) const; - void computeDiffGammaCDF(std::vector constants, double mD, double mK); public: virtual std::vector DensityVariables() const override; diff --git a/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h b/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h index 9b8111746..77324ebb9 100644 --- a/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h +++ b/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h @@ -46,7 +46,6 @@ class CharmMesonDecay3Body : public Decay { friend cereal::access; private: const std::set primary_types = {siren::dataclasses::Particle::ParticleType::D0, siren::dataclasses::Particle::ParticleType::DPlus}; - siren::utilities::Interpolator1D inverseCdf; // for dGamma (form-factor model; not used by FinalStateProbability) // Shared closure helpers: SampleFinalState's density and FinalStateProbability // both build on these, so Sample == Density by construction. static double KStarMass(); @@ -67,14 +66,11 @@ friend cereal::access; double TotalDecayWidth(siren::dataclasses::Particle::ParticleType primary) const override; double TotalDecayWidthForFinalState(dataclasses::InteractionRecord const &) const override; double DifferentialDecayWidth(dataclasses::InteractionRecord const &) const override; - double DifferentialDecayWidth(std::vector constants, double Q2, double mD, double mK) const; void SampleFinalStateHadronic(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr) const; void SampleFinalState(dataclasses::CrossSectionDistributionRecord &, std::shared_ptr) const override; std::vector GetPossibleSignatures() const override; std::vector GetPossibleSignaturesFromParent(siren::dataclasses::Particle::ParticleType primary) const override; virtual double FinalStateProbability(dataclasses::InteractionRecord const & record) const override; - std::vector FormFactorFromRecord(dataclasses::CrossSectionDistributionRecord const & record) const; - void computeDiffGammaCDF(std::vector constants, double mD, double mK); public: virtual std::vector DensityVariables() const override; diff --git a/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h b/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h index 36e5cf1a1..8fc6028fc 100644 --- a/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h +++ b/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h @@ -55,16 +55,13 @@ friend cereal::access; // Pythia instance (mutable because SampleFinalState is const) mutable std::unique_ptr pythia_; - mutable bool pythia_initialized_ = false; mutable std::shared_ptr siren_rndm_; // Signature bookkeeping std::vector signatures_; std::set primary_types_; std::set target_types_; - std::map> targets_by_primary_types_; std::map, std::vector> signatures_by_parent_types_; - std::set D_types_; // DIS parameters int interaction_type_; // 1=CC, 2=NC @@ -88,7 +85,6 @@ friend cereal::access; static bool IsCharmedHadron(int pdgId); static siren::dataclasses::ParticleType PdgToParticleType(int pdgId); static double GetLeptonMass(siren::dataclasses::ParticleType lepton_type); - static double GetHadronMass(siren::dataclasses::ParticleType hadron_type); static std::map getIndices(siren::dataclasses::InteractionSignature signature); public: diff --git a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h index 45f6e46dc..ff17b369f 100644 --- a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h +++ b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h @@ -63,9 +63,7 @@ friend cereal::access; std::vector signatures_; std::set primary_types_; std::set target_types_; - std::map> targets_by_primary_types_; std::map, std::vector> signatures_by_parent_types_; - std::set D_types_; // used by the DIS process int interaction_type_; From 685e498911bf10af6278a22b520b9427e85d150b Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sun, 28 Jun 2026 18:30:44 -0700 Subject: [PATCH 91/93] charm-DIS: dedupe shared logic into header-only helpers Move logic shared by the two decay classes and the two cross-section classes into header-only inline helpers: CharmDecayKinematics.h (KStarMass, VAWeightAngleAverage, SampledQ2Density, SampledQ2Normalization, integratePositivePartQuadratic, particleMass), CharmCrossSectionHelpers.h (fragmentation fractions, charged-lepton-product map, lepton mass, unit string, signature builders), and CharmDecayTestHelpers.h (the numeric angle-average oracle). The classes stay independent. --- .../interactions/private/CharmMesonDecay.cxx | 224 ++---------------- .../private/CharmMesonDecay3Body.cxx | 212 ++--------------- projects/interactions/private/DMesonELoss.cxx | 33 +-- .../private/PythiaDISCrossSection.cxx | 111 ++------- .../private/QuarkDISFromSpline.cxx | 163 +++---------- .../private/test/CharmDISClosure_TEST.cxx | 9 - .../private/test/CharmDecayTestHelpers.h | 65 +++++ .../test/CharmMesonDecay3Body_TEST.cxx | 69 ++---- .../private/test/CharmMesonDecay_TEST.cxx | 68 ++---- .../interactions/CharmCrossSectionHelpers.h | 110 +++++++++ .../SIREN/interactions/CharmDecayKinematics.h | 209 ++++++++++++++++ .../SIREN/interactions/CharmMesonDecay.h | 13 +- .../SIREN/interactions/CharmMesonDecay3Body.h | 28 +-- .../public/SIREN/interactions/DMesonELoss.h | 11 +- tests/python/test_pythia_charm_validation.py | 24 -- 15 files changed, 532 insertions(+), 817 deletions(-) create mode 100644 projects/interactions/private/test/CharmDecayTestHelpers.h create mode 100644 projects/interactions/public/SIREN/interactions/CharmCrossSectionHelpers.h create mode 100644 projects/interactions/public/SIREN/interactions/CharmDecayKinematics.h diff --git a/projects/interactions/private/CharmMesonDecay.cxx b/projects/interactions/private/CharmMesonDecay.cxx index be4cedce6..525e01780 100644 --- a/projects/interactions/private/CharmMesonDecay.cxx +++ b/projects/interactions/private/CharmMesonDecay.cxx @@ -17,10 +17,14 @@ #include "SIREN/utilities/Interpolator.h" #include "SIREN/interactions/Decay.h" +#include "SIREN/interactions/CharmDecayKinematics.h" namespace siren { namespace interactions { +using charm_decay::particleMass; +using charm_decay::KStarMass; + CharmMesonDecay::CharmMesonDecay() {} CharmMesonDecay::CharmMesonDecay(siren::dataclasses::Particle::ParticleType primary) {} @@ -34,46 +38,6 @@ bool CharmMesonDecay::equal(Decay const & other) const { return primary_types == x->primary_types; } - -double CharmMesonDecay::particleMass(siren::dataclasses::ParticleType particle) { - switch(particle){ - case siren::dataclasses::ParticleType::D0: - return( siren::utilities::Constants::D0Mass); - case siren::dataclasses::ParticleType::D0Bar: - return( siren::utilities::Constants::D0Mass); - case siren::dataclasses::ParticleType::DPlus: - return( siren::utilities::Constants::DPlusMass); - case siren::dataclasses::ParticleType::DMinus: - return( siren::utilities::Constants::DPlusMass); - case siren::dataclasses::ParticleType::K0: - return( siren::utilities::Constants::K0Mass); - case siren::dataclasses::ParticleType::K0Bar: - return( siren::utilities::Constants::K0Mass); - case siren::dataclasses::ParticleType::KPlus: - return( siren::utilities::Constants::KPlusMass); - case siren::dataclasses::ParticleType::KMinus: - return( siren::utilities::Constants::KMinusMass); - case siren::dataclasses::ParticleType::EPlus: - return( siren::utilities::Constants::electronMass ); - case siren::dataclasses::ParticleType::EMinus: - return( siren::utilities::Constants::electronMass ); - case siren::dataclasses::ParticleType::MuPlus: - return( siren::utilities::Constants::muonMass ); - case siren::dataclasses::ParticleType::MuMinus: - return( siren::utilities::Constants::muonMass ); - case siren::dataclasses::ParticleType::TauPlus: - return( siren::utilities::Constants::tauMass ); - case siren::dataclasses::ParticleType::TauMinus: - return( siren::utilities::Constants::tauMass ); - case siren::dataclasses::ParticleType::DsPlus: - return( siren::utilities::Constants::DsPlusMass ); - case siren::dataclasses::ParticleType::DsMinus: - return( siren::utilities::Constants::DsMinusMass ); - default: - return(0.0); - } -} - double CharmMesonDecay::TotalDecayWidth(dataclasses::InteractionRecord const & record) const { return TotalDecayWidth(record.signature.primary_type); } @@ -329,19 +293,12 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco return; } - // ========================================================================= - // 3-body phase space sampling following Pythia's approach - // (ParticleDecays::threeBody in ParticleDecays.cc) - // - // D (m0) -> hadron (m1) + lepton (m2) + neutrino (m3) - // - // Phase space: sample m23 (lepton-neutrino invariant mass = sqrt(q^2)) - // flat in allowed range, accept-reject on phase space weight. - // For D+/D0: apply V-A matrix element correction (K vs K* with fixed ratio). - // For Ds: pure 3-body phase space, no V-A correction. Daughter is - // sampled inline as eta / eta' / phi with fractions 0.46 / 0.16 - // / 0.38 (from Ds->eta/eta'/phi mu nu BRs of 2.3/0.8/1.9 %). - // ========================================================================= + // 3-body phase space (Pythia ParticleDecays::threeBody): + // D (m0) -> hadron (m1) + lepton (m2) + neutrino (m3). Sample + // m23 = sqrt(q^2) flat with accept-reject on the phase-space weight. + // D+/D0: V-A matrix-element correction with K vs K* mixing. + // Ds: pure phase space, daughter sampled inline as eta/eta'/phi + // (fractions 0.46/0.16/0.38 from BRs 2.3/0.8/1.9 %). siren::dataclasses::Particle::ParticleType primary = record.signature.primary_type; bool is_Ds = (primary == siren::dataclasses::Particle::ParticleType::DsPlus || @@ -505,159 +462,10 @@ void CharmMesonDecay::SampleFinalState(dataclasses::CrossSectionDistributionReco } -// --------------------------------------------------------------------------- -// Closure-correct FinalStateProbability machinery. -// -// FinalStateProbability MUST be the exact normalized q^2 density that -// SampleFinalState produces (the Weighter consumes it as the physical -// final-state density). The form-factor DifferentialDecayWidth above is a -// separate physical quantity that the SAMPLER never used, so it cannot be the -// closure density. The sampler draws m23 = sqrt(q^2) flat with accept-reject -// on the phase-space weight p1Abs*p23Abs, then (for D+/D0) accept-rejects on -// the V-A weight wtME = mD*E_l*(p_nu . p_K). The marginal density of m23 of -// accepted events is therefore proportional to -// p1Abs(m23) * p23Abs(m23) * _angle , -// and the density in q^2 carries the Jacobian dm23/dq^2 = 1/(2 m23). The -// helpers below reproduce exactly this density; both the per-event numerator -// and the normalization integral use the same SampledQ2Density, so -// Sample == Density by construction. -// --------------------------------------------------------------------------- - -double CharmMesonDecay::KStarMass() { - // Single source of truth for the K*(892) mass shared by the sampler and the - // FinalStateProbability normalizer. - return siren::utilities::Constants::KPrimePlusMass; -} - -namespace { -// Integral over [lo, hi] of max(0, a2*c^2 + a1*c + a0). Closed form with a -// linear/constant fallback for a2 ~ 0 and numerically stable quadratic roots. -// Used to evaluate the angle-averaged V-A weight analytically. -double integratePositivePartQuadratic(double a2, double a1, double a0, - double lo, double hi) { - if (hi <= lo) return 0.0; - auto F = [&](double c) { return a2 * c * c * c / 3.0 + a1 * c * c / 2.0 + a0 * c; }; - auto seg = [&](double x0, double x1) -> double { - if (x1 <= x0) return 0.0; - return F(x1) - F(x0); - }; - double scale = std::abs(a1) + std::abs(a0) + 1.0; - if (std::abs(a2) < 1e-12 * scale) { - // Linear q(c) = a1*c + a0 (or constant if a1 ~ 0). - if (std::abs(a1) < 1e-300) return (a0 > 0.0) ? seg(lo, hi) : 0.0; - double root = -a0 / a1; - if (a1 > 0.0) return seg(std::max(lo, root), hi); // q > 0 for c > root - return seg(lo, std::min(hi, root)); // q > 0 for c < root - } - double disc = a1 * a1 - 4.0 * a2 * a0; - if (disc <= 0.0) { - // No real roots: q keeps the sign of a2 everywhere. - return (a2 > 0.0) ? seg(lo, hi) : 0.0; - } - double sq = std::sqrt(disc); - double qq = -0.5 * (a1 + ((a1 >= 0.0) ? sq : -sq)); // stable roots - double r1 = qq / a2; - double r2 = a0 / qq; - double rlo = std::min(r1, r2), rhi = std::max(r1, r2); - if (a2 > 0.0) { - // q > 0 outside [rlo, rhi]. - return seg(lo, std::min(hi, rlo)) + seg(std::max(lo, rhi), hi); - } - // q > 0 inside (rlo, rhi). - return seg(std::max(lo, rlo), std::min(hi, rhi)); -} -} // namespace - -double CharmMesonDecay::VAWeightAngleAverage(double mD, double mK, double ml, double m23) const { - // Analytic angle-average of the sampler's ACCEPTED V-A weight, - // (1/2) * integral_{-1}^{1} clamp(wtME(c), 0, wtMEmax) dc, - // where wtME(c) = mD * E_l(c) * (p_nu(c) . p_K) and c = cos(theta_lepton) in - // the (l,nu) rest frame. After the boost to the D rest frame both E_l and - // (p_nu . p_K) are linear in c, so wtME = pref * (a0 + a1 c + a2 c^2) is an - // exact quadratic. clamp(q, 0, M) = max(0, q) - max(0, q - M) turns the - // accept-reject ceiling into two positive-part integrals, each closed form. - // This reproduces SampleFinalState's accepted density exactly (weighting - // closure) with no quadrature error; the same integral is also evaluated - // numerically in the unit tests as a cross-check. - double mnu = 0.0; - double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) - * (mD + mK - m23) * (mD - mK + m23)) / mD; - double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) - * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; - if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; - double E23 = std::sqrt(p1Abs * p1Abs + m23 * m23); - double bz = -p1Abs / E23; // (l,nu)-system velocity along -kaon axis - double gamma = E23 / m23; - double Elrest = std::sqrt(p23Abs * p23Abs + ml * ml); - double EK = std::sqrt(p1Abs * p1Abs + mK * mK); - // E_l = gamma (C + D c); (p_nu . p_K) = gamma p23Abs (A + B c). - double C = Elrest; - double D = bz * p23Abs; - double A = EK - p1Abs * bz; - double B = p1Abs - EK * bz; - double pref = mD * gamma * gamma * p23Abs; // > 0 - if (pref <= 0.0) return 0.0; - // wtME = pref * q(c), q(c) = (C + D c)(A + B c) = a0 + a1 c + a2 c^2. - double a0 = C * A; - double a1 = C * B + D * A; - double a2 = D * B; - // Same matrix-element ceiling as SampleFinalState (meMode == 22). - double wtMEmax = std::min(std::pow(mD, 4) / 16.0, - mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); - double qmax = wtMEmax / pref; - double integral = integratePositivePartQuadratic(a2, a1, a0, -1.0, 1.0) - - integratePositivePartQuadratic(a2, a1, a0 - qmax, -1.0, 1.0); - return 0.5 * pref * integral; // average over c (measure width 2) -} - -double CharmMesonDecay::SampledQ2Density(double mD, double mK, double ml, double q2, bool apply_va) const { - // Unnormalized sampler density in q^2 for a single hadron-mass component. - double m23 = std::sqrt(q2); - double m23Max = mD - mK; - if (m23 <= (ml) || m23 >= m23Max) return 0.0; - double mnu = 0.0; - double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) - * (mD + mK - m23) * (mD - mK + m23)) / mD; - double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) - * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; - if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; - // Phase-space weight WITH the sampler's rejection ceiling. SampleFinalState - // accept-rejects m23 against wtPSmax = 0.5*p1Max*p23Max, which p1Abs*p23Abs can - // exceed; in that regime the sampler saturates (always accepts), so the - // accepted density is flat at wtPSmax. Reproduce the clip for exact closure. - double m23Min = ml + mnu; - double p1Max = 0.5 * std::sqrt((mD - mK - m23Min) * (mD + mK + m23Min) - * (mD + mK - m23Min) * (mD - mK + m23Min)) / mD; - double p23Max = 0.5 * std::sqrt((m23Max - ml - mnu) * (m23Max + ml + mnu) - * (m23Max + ml - mnu) * (m23Max - ml + mnu)) / m23Max; - double wtPSmax = 0.5 * p1Max * p23Max; - double wtPS = p1Abs * p23Abs; - if (wtPS > wtPSmax) wtPS = wtPSmax; - // V-A angle-averaged weight (D+/D0) or 1 for pure phase space (Ds). - double me = apply_va ? VAWeightAngleAverage(mD, mK, ml, m23) : 1.0; - // Jacobian dm23/dq^2 = 1/(2 m23) converts the m23 density to a q^2 density. - return wtPS * me / (2.0 * m23); -} - -double CharmMesonDecay::SampledQ2Normalization(double mD, double mK, double ml, bool apply_va) const { - // Integral of SampledQ2Density over the kinematically allowed q^2 range, used - // to normalize each mixture component to a proper pdf. Cached per - // (mD, mK, ml, apply_va) so the per-event FinalStateProbability call does not - // re-integrate (the kinematics are fixed by the primary/daughter masses). - double key = ((mD * 1e3 + mK) * 1e3 + ml) * 2.0 + (apply_va ? 1.0 : 0.0); - long ikey = (long)std::llround(key * 1e3); - auto it = norm_cache.find(ikey); - if (it != norm_cache.end()) return it->second; - double q2min = ml * ml; - double q2max = (mD - mK) * (mD - mK); - std::function integrand = [&](double q2) -> double { - return SampledQ2Density(mD, mK, ml, q2, apply_va); - }; - double n = siren::utilities::rombergIntegrate(integrand, q2min, q2max); - norm_cache[ikey] = n; - return n; -} - +// FinalStateProbability is the normalized q^2 density SampleFinalState produces +// (closure rationale in CharmDecayKinematics.h). It builds the same hadron-mass +// mixture the sampler used, evaluates charm_decay::SampledQ2Density for the +// recorded component, and normalizes via charm_decay::SampledQ2Normalization. double CharmMesonDecay::FinalStateProbability(dataclasses::InteractionRecord const & record) const { dataclasses::InteractionSignature signature = record.signature; // Guard: finalize() does not copy the signature, so a finalized record can @@ -726,9 +534,9 @@ double CharmMesonDecay::FinalStateProbability(dataclasses::InteractionRecord con } if (comp < 0) return 0.0; - double g = SampledQ2Density(mD, masses[comp], ml, q2, apply_va); + double g = charm_decay::SampledQ2Density(mD, masses[comp], ml, q2, apply_va); if (g <= 0.0) return 0.0; - double norm = SampledQ2Normalization(mD, masses[comp], ml, apply_va); + double norm = charm_decay::SampledQ2Normalization(mD, masses[comp], ml, apply_va, norm_cache); if (norm <= 0.0) return 0.0; // FinalStateProbability = mixture_fraction * (component density / component norm), diff --git a/projects/interactions/private/CharmMesonDecay3Body.cxx b/projects/interactions/private/CharmMesonDecay3Body.cxx index d58ad2f6b..d5a756f8a 100644 --- a/projects/interactions/private/CharmMesonDecay3Body.cxx +++ b/projects/interactions/private/CharmMesonDecay3Body.cxx @@ -17,10 +17,14 @@ #include "SIREN/utilities/Interpolator.h" #include "SIREN/interactions/Decay.h" +#include "SIREN/interactions/CharmDecayKinematics.h" namespace siren { namespace interactions { +using charm_decay::particleMass; +using charm_decay::KStarMass; + CharmMesonDecay3Body::CharmMesonDecay3Body() {} CharmMesonDecay3Body::CharmMesonDecay3Body(siren::dataclasses::Particle::ParticleType primary) { @@ -39,42 +43,6 @@ bool CharmMesonDecay3Body::equal(Decay const & other) const { return primary_types == x->primary_types; } - -double CharmMesonDecay3Body::particleMass(siren::dataclasses::ParticleType particle) { - switch(particle){ - case siren::dataclasses::ParticleType::D0: - return( siren::utilities::Constants::D0Mass); - case siren::dataclasses::ParticleType::D0Bar: - return( siren::utilities::Constants::D0Mass); - case siren::dataclasses::ParticleType::DPlus: - return( siren::utilities::Constants::DPlusMass); - case siren::dataclasses::ParticleType::DMinus: - return( siren::utilities::Constants::DPlusMass); - case siren::dataclasses::ParticleType::K0: - return( siren::utilities::Constants::K0Mass); - case siren::dataclasses::ParticleType::K0Bar: - return( siren::utilities::Constants::K0Mass); - case siren::dataclasses::ParticleType::KPlus: - return( siren::utilities::Constants::KPlusMass); - case siren::dataclasses::ParticleType::KMinus: - return( siren::utilities::Constants::KMinusMass); - case siren::dataclasses::ParticleType::EPlus: - return( siren::utilities::Constants::electronMass ); - case siren::dataclasses::ParticleType::EMinus: - return( siren::utilities::Constants::electronMass ); - case siren::dataclasses::ParticleType::MuPlus: - return( siren::utilities::Constants::muonMass ); - case siren::dataclasses::ParticleType::MuMinus: - return( siren::utilities::Constants::muonMass ); - case siren::dataclasses::ParticleType::TauPlus: - return( siren::utilities::Constants::tauMass ); - case siren::dataclasses::ParticleType::TauMinus: - return( siren::utilities::Constants::tauMass ); - default: - return(0.0); - } -} - double CharmMesonDecay3Body::TotalDecayWidth(dataclasses::InteractionRecord const & record) const { return TotalDecayWidth(record.signature.primary_type); } @@ -223,29 +191,16 @@ void CharmMesonDecay3Body::SampleFinalState(dataclasses::CrossSectionDistributio return; } - // ========================================================================= - // 3-body phase space sampling following Pythia's approach - // (ParticleDecays::threeBody in ParticleDecays.cc) - // - // D (m0) -> K (m1) + lepton (m2) + neutrino (m3) + // 3-body phase space (Pythia ParticleDecays::threeBody): + // D (m0) -> K (m1) + lepton (m2) + neutrino (m3). Sample + // m23 = sqrt(q^2) flat with accept-reject on the phase-space weight, then + // apply the V-A matrix-element correction. // - // Phase space: sample m23 (lepton-neutrino invariant mass = sqrt(q^2)) - // flat in allowed range, accept-reject on phase space weight. - // Then apply V-A matrix element correction. - // - // K / K*(892) mixing: a fraction (1 - fracK) of events use mK*=0.892 GeV - // as the hadron mass instead of the pseudoscalar K mass, which broadens - // the lepton spectrum through phase-space and matrix-element effects. - // - // NOTE (design decision): this is *kinematic* K/K* mixing only. The - // secondary particle's ParticleType is always left at the K species from - // the signature (K0bar / K-), even when the K* mass was drawn. We do not - // advertise separate K* signatures in GetPossibleSignaturesFromParent() - // and we do not re-type the secondary. This is intentional -- for our use - // case (weighting lepton/neutrino kinematics correctly in the presence of - // the resonant K* contribution) the mass treatment is what matters, and - // downstream propagation treats the secondary as a pseudoscalar K. - // ========================================================================= + // K/K*(892) mixing is KINEMATIC only: a fraction (1 - fracK) of events draw + // the K*(892) mass for the hadron, broadening the lepton spectrum. The + // secondary's ParticleType stays the signature K species (K0bar / K-) -- we + // neither advertise separate K* signatures nor re-type the secondary, since + // only the mass matters for weighting the lepton/neutrino kinematics. double mD = particleMass(record.signature.primary_type); double mK_base = particleMass(record.signature.secondary_types[0]); // K mass from signature @@ -362,139 +317,10 @@ void CharmMesonDecay3Body::SampleFinalState(dataclasses::CrossSectionDistributio neutrino.SetHelicity(record.primary_helicity); } -// --------------------------------------------------------------------------- -// Closure-correct FinalStateProbability machinery (see CharmMesonDecay.cxx for -// the full derivation). FinalStateProbability MUST be the normalized q^2 -// density that SampleFinalState produces: the sampler draws m23 = sqrt(q^2) -// flat with accept-reject on p1Abs*p23Abs, then accept-rejects on the V-A -// weight wtME = mD*E_l*(p_nu . p_K). The accepted m23 density is therefore -// proportional to p1Abs*p23Abs*_angle, and the q^2 density carries -// the Jacobian 1/(2 m23). Both the numerator and the per-component normalizer -// use the same SampledQ2Density, so Sample == Density by construction. The -// form-factor DifferentialDecayWidth is a separate quantity the sampler never -// used and is NOT the closure density. -// --------------------------------------------------------------------------- - -double CharmMesonDecay3Body::KStarMass() { - // Single source of truth for the K*(892) mass shared by the sampler and the - // FinalStateProbability normalizer. - return siren::utilities::Constants::KPrimePlusMass; -} - -namespace { -// Integral over [lo, hi] of max(0, a2*c^2 + a1*c + a0). Closed form with a -// linear/constant fallback for a2 ~ 0 and numerically stable quadratic roots. -double integratePositivePartQuadratic(double a2, double a1, double a0, - double lo, double hi) { - if (hi <= lo) return 0.0; - auto F = [&](double c) { return a2 * c * c * c / 3.0 + a1 * c * c / 2.0 + a0 * c; }; - auto seg = [&](double x0, double x1) -> double { - if (x1 <= x0) return 0.0; - return F(x1) - F(x0); - }; - double scale = std::abs(a1) + std::abs(a0) + 1.0; - if (std::abs(a2) < 1e-12 * scale) { - if (std::abs(a1) < 1e-300) return (a0 > 0.0) ? seg(lo, hi) : 0.0; - double root = -a0 / a1; - if (a1 > 0.0) return seg(std::max(lo, root), hi); - return seg(lo, std::min(hi, root)); - } - double disc = a1 * a1 - 4.0 * a2 * a0; - if (disc <= 0.0) { - return (a2 > 0.0) ? seg(lo, hi) : 0.0; - } - double sq = std::sqrt(disc); - double qq = -0.5 * (a1 + ((a1 >= 0.0) ? sq : -sq)); - double r1 = qq / a2; - double r2 = a0 / qq; - double rlo = std::min(r1, r2), rhi = std::max(r1, r2); - if (a2 > 0.0) { - return seg(lo, std::min(hi, rlo)) + seg(std::max(lo, rhi), hi); - } - return seg(std::max(lo, rlo), std::min(hi, rhi)); -} -} // namespace - -double CharmMesonDecay3Body::VAWeightAngleAverage(double mD, double mK, double ml, double m23) const { - // Analytic angle-average of the sampler's ACCEPTED V-A weight, - // (1/2) * integral_{-1}^{1} clamp(wtME(c), 0, wtMEmax) dc. - // wtME(c) = mD * E_l(c) * (p_nu(c) . p_K) is an exact quadratic in - // c = cos(theta_lepton) after the boost to the D rest frame, so the positive - // and clipped parts integrate in closed form via - // clamp(q, 0, M) = max(0, q) - max(0, q - M). Matches SampleFinalState's - // accepted density exactly (closure) with no quadrature error; the same - // integral is also evaluated numerically in the unit tests as a cross-check. - double mnu = 0.0; - double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) - * (mD + mK - m23) * (mD - mK + m23)) / mD; - double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) - * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; - if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; - double E23 = std::sqrt(p1Abs * p1Abs + m23 * m23); - double bz = -p1Abs / E23; - double gamma = E23 / m23; - double Elrest = std::sqrt(p23Abs * p23Abs + ml * ml); - double EK = std::sqrt(p1Abs * p1Abs + mK * mK); - double C = Elrest; - double D = bz * p23Abs; - double A = EK - p1Abs * bz; - double B = p1Abs - EK * bz; - double pref = mD * gamma * gamma * p23Abs; - if (pref <= 0.0) return 0.0; - double a0 = C * A; - double a1 = C * B + D * A; - double a2 = D * B; - double wtMEmax = std::min(std::pow(mD, 4) / 16.0, - mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); - double qmax = wtMEmax / pref; - double integral = integratePositivePartQuadratic(a2, a1, a0, -1.0, 1.0) - - integratePositivePartQuadratic(a2, a1, a0 - qmax, -1.0, 1.0); - return 0.5 * pref * integral; -} - -double CharmMesonDecay3Body::SampledQ2Density(double mD, double mK, double ml, double q2, bool apply_va) const { - double m23 = std::sqrt(q2); - double m23Max = mD - mK; - if (m23 <= ml || m23 >= m23Max) return 0.0; - double mnu = 0.0; - double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) - * (mD + mK - m23) * (mD - mK + m23)) / mD; - double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) - * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; - if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; - // Phase-space weight WITH the sampler's rejection ceiling. SampleFinalState - // accept-rejects m23 against wtPSmax = 0.5*p1Max*p23Max, which p1Abs*p23Abs can - // exceed; there the sampler saturates (always accepts), so the accepted - // density is flat at wtPSmax. Reproduce the clip for exact closure. - double m23Min = ml + mnu; - double p1Max = 0.5 * std::sqrt((mD - mK - m23Min) * (mD + mK + m23Min) - * (mD + mK - m23Min) * (mD - mK + m23Min)) / mD; - double p23Max = 0.5 * std::sqrt((m23Max - ml - mnu) * (m23Max + ml + mnu) - * (m23Max + ml - mnu) * (m23Max - ml + mnu)) / m23Max; - double wtPSmax = 0.5 * p1Max * p23Max; - double wtPS = p1Abs * p23Abs; - if (wtPS > wtPSmax) wtPS = wtPSmax; - double me = apply_va ? VAWeightAngleAverage(mD, mK, ml, m23) : 1.0; - return wtPS * me / (2.0 * m23); -} - -double CharmMesonDecay3Body::SampledQ2Normalization(double mD, double mK, double ml, bool apply_va) const { - // Cached per (mD, mK, ml, apply_va) so per-event FinalStateProbability does - // not re-integrate the fixed-kinematics normalizer. - double key = ((mD * 1e3 + mK) * 1e3 + ml) * 2.0 + (apply_va ? 1.0 : 0.0); - long ikey = (long)std::llround(key * 1e3); - auto it = norm_cache.find(ikey); - if (it != norm_cache.end()) return it->second; - double q2min = ml * ml; - double q2max = (mD - mK) * (mD - mK); - std::function integrand = [&](double q2) -> double { - return SampledQ2Density(mD, mK, ml, q2, apply_va); - }; - double n = siren::utilities::rombergIntegrate(integrand, q2min, q2max); - norm_cache[ikey] = n; - return n; -} - +// FinalStateProbability is the normalized q^2 density SampleFinalState produces +// (closure rationale in CharmDecayKinematics.h). Builds the same K/K*(892) +// mixture the sampler used, evaluates charm_decay::SampledQ2Density for the +// recorded component, and normalizes via charm_decay::SampledQ2Normalization. double CharmMesonDecay3Body::FinalStateProbability(dataclasses::InteractionRecord const & record) const { dataclasses::InteractionSignature signature = record.signature; // Guard: finalize() does not copy the signature, so a finalized record can @@ -540,9 +366,9 @@ double CharmMesonDecay3Body::FinalStateProbability(dataclasses::InteractionRecor } if (comp < 0) return 0.0; - double g = SampledQ2Density(mD, masses[comp], ml, q2, apply_va); + double g = charm_decay::SampledQ2Density(mD, masses[comp], ml, q2, apply_va); if (g <= 0.0) return 0.0; - double norm = SampledQ2Normalization(mD, masses[comp], ml, apply_va); + double norm = charm_decay::SampledQ2Normalization(mD, masses[comp], ml, apply_va, norm_cache); if (norm <= 0.0) return 0.0; // Normalized q^2 pdf summing over the K/K* mixture. diff --git a/projects/interactions/private/DMesonELoss.cxx b/projects/interactions/private/DMesonELoss.cxx index 6b157ff5c..debca2e3f 100644 --- a/projects/interactions/private/DMesonELoss.cxx +++ b/projects/interactions/private/DMesonELoss.cxx @@ -114,13 +114,11 @@ double DMesonELoss::DifferentialCrossSection(dataclasses::InteractionRecord cons double final_energy = interaction.secondary_momenta[0][0]; double z = 1 - final_energy / primary_energy; - // The density support MUST match the sampler's realized support, or - // FinalStateProbability is mis-normalized (closure break, worst at low primary - // energy). SampleFinalState accepts z in [z_min_, z_max_] AND enforces the - // energy-dependent kinematic cut final_energy >= Dmass, i.e. z <= 1 - Dmass/E. - // Apply the same cut here and normalize the Gaussian over the identical - // [z_min_, z_hi] interval. z_hi collapses below z_min_ for sub-threshold - // primaries (E <= Dmass), where there is no valid final state. + // Density support must match the sampler's realized support or + // FinalStateProbability is mis-normalized (closure break, worst at low E). + // Apply the same energy-dependent cut z <= 1 - Dmass/E the sampler enforces and + // normalize the Gaussian over [z_min_, z_hi]; z_hi collapses below z_min_ for + // sub-threshold primaries (E <= Dmass), where there is no valid final state. double z_kin = 1.0 - Dmass / primary_energy; // kinematic upper limit double z_hi = (z_max_ < z_kin) ? z_max_ : z_kin; if(z_hi <= z_min_ || z < z_min_ || z > z_hi) { @@ -193,11 +191,9 @@ void DMesonELoss::SampleFinalState(dataclasses::CrossSectionDistributionRecord& double z = sigma * sqrt(-2.0 * log(u1)) * cos(2.0 * M_PI * u2) + z0; // now modify the energy of the charm hadron and the corresponding momentum final_energy = primary_energy * (1-z); - // Reject z outside [z_min_, z_max_] so the realized sampling density is the - // truncated Gaussian that DifferentialCrossSection/FinalStateProbability - // normalize over the same interval (closure). z < z_min_ would otherwise let - // the D meson GAIN energy (final_energy > primary_energy). The kinematic cut - // final_energy^2 >= Dmass^2 is a defensive guard. + // Reject z outside [z_min_, z_max_] so the realized density matches the + // truncated Gaussian the density normalizes over (closure). z < z_min_ would + // let the D GAIN energy; final_energy^2 >= Dmass^2 is a defensive guard. accept = (z >= z_min_) && (z <= z_max_) && (pow(final_energy, 2) - pow(Dmass, 2) >= 0); } while (!accept); @@ -235,15 +231,10 @@ double DMesonELoss::FinalStateProbability(dataclasses::InteractionRecord const & } } -// NOTE: SampleFinalState samples a single inelasticity DOF z (the D meson is set -// collinear with the parent, so there is no independent azimuth). That same z is -// fully captured by the density: DifferentialCrossSection reconstructs -// z = 1 - final_energy/primary_energy and FinalStateProbability returns the -// (truncated, normalized) Gaussian in z. Sampled DOF == density DOF, so this class -// is closure-safe in the standard unbiased configuration (the same cross-section -// object supplies both the injection and physical densities). Like the other charm -// cross sections here, BIASING the D kinematics with a separate phase-space channel -// is NOT supported and would produce incorrect weights. +// SampleFinalState samples a single inelasticity DOF z (D set collinear, no +// azimuth), fully captured by the density (truncated, normalized Gaussian in z). +// Sampled DOF == density DOF, so this is closure-safe in the unbiased config. +// Biasing the D kinematics with a separate phase-space channel is unsupported. std::vector DMesonELoss::DensityVariables() const { return std::vector{"Bjorken y"}; } diff --git a/projects/interactions/private/PythiaDISCrossSection.cxx b/projects/interactions/private/PythiaDISCrossSection.cxx index 6bc806adc..da43860e9 100644 --- a/projects/interactions/private/PythiaDISCrossSection.cxx +++ b/projects/interactions/private/PythiaDISCrossSection.cxx @@ -24,6 +24,7 @@ #include #include "SIREN/interactions/CrossSection.h" +#include "SIREN/interactions/CharmCrossSectionHelpers.h" #include "SIREN/dataclasses/InteractionRecord.h" #include "SIREN/dataclasses/Particle.h" #include "SIREN/utilities/Random.h" @@ -132,15 +133,7 @@ PythiaDISCrossSection::PythiaDISCrossSection( // --- File I/O --- void PythiaDISCrossSection::SetUnits(std::string units) { - std::transform(units.begin(), units.end(), units.begin(), - [](unsigned char c){ return std::tolower(c); }); - if(units == "cm") { - unit = 1.0; - } else if(units == "m") { - unit = 10000.0; - } else { - throw std::runtime_error("Cross section units not supported!"); - } + unit = charm_xsec::UnitForString(units); } void PythiaDISCrossSection::LoadFromFile(std::string dd_crossSectionFile, std::string total_crossSectionFile) { @@ -204,14 +197,7 @@ siren::dataclasses::ParticleType PythiaDISCrossSection::PdgToParticleType(int pd } double PythiaDISCrossSection::GetLeptonMass(siren::dataclasses::ParticleType lepton_type) { - int32_t lepton_number = std::abs(static_cast(lepton_type)); - switch(lepton_number) { - case 11: return siren::utilities::Constants::electronMass; - case 13: return siren::utilities::Constants::muonMass; - case 15: return siren::utilities::Constants::tauMass; - case 12: case 14: case 16: return 0; - default: throw std::runtime_error("Unknown lepton type!"); - } + return charm_xsec::GetLeptonMass(lepton_type); } std::map PythiaDISCrossSection::getIndices(siren::dataclasses::InteractionSignature signature) { @@ -236,11 +222,9 @@ std::map PythiaDISCrossSection::getIndices(siren::dataclasses: // --- Signatures --- void PythiaDISCrossSection::InitializeSignatures() { - // PythiaDISCrossSection forces charm only in charged-current (the non-charm - // CKM elements are zeroed in ApplyPythiaCharmConfig). Z exchange has no such - // handle, so NC charm cannot be forced and SampleFinalState would exhaust its - // attempt budget and throw. Reject NC at construction with an actionable - // pointer to the tool that does support NC charm (QuarkDISFromSpline). + // Charm is forced only in CC (non-charm CKM zeroed in ApplyPythiaCharmConfig). + // Z exchange has no such handle, so NC charm can't be forced and sampling would + // exhaust its budget; reject NC at construction (use QuarkDISFromSpline for NC). if(interaction_type_ != 1) { if(interaction_type_ == 2) throw std::runtime_error("PythiaDISCrossSection supports only charged-current charm production (interaction_type=1). Neutral-current charm is not forced by Z exchange in Pythia, so per-event sampling would fail; use QuarkDISFromSpline for NC charm."); @@ -254,25 +238,9 @@ void PythiaDISCrossSection::InitializeSignatures() { throw std::runtime_error("PythiaDISCrossSection only supports neutrinos as primaries!"); } - siren::dataclasses::ParticleType charged_lepton_product = siren::dataclasses::ParticleType::unknown; + siren::dataclasses::ParticleType charged_lepton_product = charm_xsec::ChargedLeptonProduct(primary_type); siren::dataclasses::ParticleType neutral_lepton_product = primary_type; - if(primary_type == siren::dataclasses::ParticleType::NuE) { - charged_lepton_product = siren::dataclasses::ParticleType::EMinus; - } else if(primary_type == siren::dataclasses::ParticleType::NuEBar) { - charged_lepton_product = siren::dataclasses::ParticleType::EPlus; - } else if(primary_type == siren::dataclasses::ParticleType::NuMu) { - charged_lepton_product = siren::dataclasses::ParticleType::MuMinus; - } else if(primary_type == siren::dataclasses::ParticleType::NuMuBar) { - charged_lepton_product = siren::dataclasses::ParticleType::MuPlus; - } else if(primary_type == siren::dataclasses::ParticleType::NuTau) { - charged_lepton_product = siren::dataclasses::ParticleType::TauMinus; - } else if(primary_type == siren::dataclasses::ParticleType::NuTauBar) { - charged_lepton_product = siren::dataclasses::ParticleType::TauPlus; - } else { - throw std::runtime_error("InitializeSignatures: Unknown parent neutrino type!"); - } - if(interaction_type_ == 1) { signature.secondary_types.push_back(charged_lepton_product); } else if(interaction_type_ == 2) { @@ -284,12 +252,10 @@ void PythiaDISCrossSection::InitializeSignatures() { // Hadron remnant signature.secondary_types.push_back(siren::dataclasses::ParticleType::Hadrons); - // Charmed meson types. For nu the c quark fragments to D0/D+/Ds+; for nubar - // the cbar quark fragments to Dbar0/D-/Ds-. SampleFinalState writes Pythia's - // actual produced PID into the signature's meson slot, so the registered set - // must include the correct charge to keep weighter signature lookups in range - // (otherwise event_weight would be NaN). - // TODO: Add Lambda_c (4122) support. + // Charmed meson types: nu -> D0/D+/Ds+, nubar -> Dbar0/D-/Ds-. + // SampleFinalState writes Pythia's actual PID into the meson slot, so the + // registered set must carry the correct charge or weighter lookups go out of + // range (NaN weight). TODO: Add Lambda_c (4122) support. bool is_antineutrino = (primary_type == siren::dataclasses::ParticleType::NuEBar || primary_type == siren::dataclasses::ParticleType::NuMuBar || @@ -326,11 +292,8 @@ double PythiaDISCrossSection::TotalCrossSection(dataclasses::InteractionRecord c if(primary_energy < InteractionThreshold(interaction)) return 0; double total_xs = TotalCrossSection(primary_type, primary_energy); // Partition the inclusive charm cross section across D species by fragmentation - // fraction for the specific D meson in this signature. The total spline holds the - // single inclusive charm cross section, so without this each of the registered - // D-type signatures (D0 + D+ + Ds) would return the full value and summing over - // them -- as the base-class TotalCrossSectionAllFinalStates and the Weighter do -- - // would triple-count charm production. Mirrors QuarkDISFromSpline::TotalCrossSection. + // fraction; without this, summing the three registered D-type signatures would + // triple-count charm production. Mirrors QuarkDISFromSpline::TotalCrossSection. for(auto const & sec_type : interaction.signature.secondary_types) { if(siren::dataclasses::isD(sec_type)) { total_xs *= FragmentationFraction(sec_type); @@ -415,9 +378,8 @@ double PythiaDISCrossSection::DifferentialCrossSection(double energy, double x, if(!has_differential_) throw std::runtime_error("PythiaDISCrossSection::DifferentialCrossSection called but no differential spline was loaded (the unbiased default uses a constant FinalStateProbability)."); double log_energy = log10(energy); - // Out of spline SUPPORT -> raise, never silently return 0: a silent zero on a - // genuinely sampled event would bias that event's physical density (and hence - // its weight) to zero. The energy extent and the (x, y) grid define validity. + // Out of spline support -> raise, never silently return 0 (would bias the + // event's weight to zero). Energy extent and the (x,y) grid define validity. if(log_energy < differential_cross_section_.lower_extent(0) || log_energy > differential_cross_section_.upper_extent(0)) throw std::runtime_error("PythiaDISCrossSection: energy " + std::to_string(energy) @@ -449,36 +411,17 @@ double PythiaDISCrossSection::InteractionThreshold(dataclasses::InteractionRecor // --- Fragmentation fractions --- double PythiaDISCrossSection::FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const { - // Approximate fractions from Pythia (charm hadronization), renormalized to - // sum to 1.0 over the implemented D species. Raw fractions D0:D+/-:Ds = - // 0.60:0.23:0.15 sum to 0.98 because the Lambda_c channel is not modeled; the - // unmodeled Lambda_c fraction is redistributed by dividing each by 0.98 so the - // partitioned signatures exactly recover the inclusive charm cross section. - // Values kept in lockstep with QuarkDISFromSpline::FragmentationFraction. - if (secondary == siren::dataclasses::ParticleType::D0 || secondary == siren::dataclasses::ParticleType::D0Bar) { - return 0.6 / 0.98; - } else if (secondary == siren::dataclasses::ParticleType::DPlus || secondary == siren::dataclasses::ParticleType::DMinus) { - return 0.23 / 0.98; - } else if (secondary == siren::dataclasses::ParticleType::DsPlus || secondary == siren::dataclasses::ParticleType::DsMinus) { - return 0.15 / 0.98; - } - // Lambda_c (~0.09) not yet implemented; its fraction is folded into the above. - return 0; + return charm_xsec::FragmentationFraction(secondary); } double PythiaDISCrossSection::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { - // The final state is sampled by Pythia, whose per-event density is not - // analytically available. With NO differential spline we return a constant: - // in the standard unbiased configuration the same cross-section object - // supplies both the injection and physical densities, so this factor cancels - // in the weight ratio and only TotalCrossSection (interaction depth / - // position / survival) matters. Biasing the final-state kinematics is not - // supported in this mode. + // Pythia's per-event density is not analytic. With NO differential spline, + // return a constant: in the unbiased config it cancels in the weight ratio and + // only TotalCrossSection matters (biasing the final state is unsupported). if(!has_differential_) return 1.0; - // A Pythia-derived differential spline was supplied: report the true density - // dsigma/sigma so weights remain correct under reweighting. The fragmentation - // fraction in TotalCrossSection cancels per-signature in CrossSectionProbability. + // With a Pythia-derived differential spline, report dsigma/sigma. The + // fragmentation fraction in TotalCrossSection cancels per-signature. double dxs = DifferentialCrossSection(interaction); double txs = TotalCrossSection(interaction); if (!std::isfinite(dxs) || !std::isfinite(txs) || dxs <= 0 || txs <= 0) return 0.0; @@ -490,11 +433,11 @@ double PythiaDISCrossSection::FinalStateProbability(dataclasses::InteractionReco // --- Signature accessors --- std::vector PythiaDISCrossSection::GetPossiblePrimaries() const { - return std::vector(primary_types_.begin(), primary_types_.end()); + return charm_xsec::ToVector(primary_types_); } std::vector PythiaDISCrossSection::GetPossibleTargetsFromPrimary(siren::dataclasses::ParticleType primary_type) const { - return std::vector(target_types_.begin(), target_types_.end()); + return charm_xsec::ToVector(target_types_); } std::vector PythiaDISCrossSection::GetPossibleSignatures() const { @@ -502,15 +445,11 @@ std::vector PythiaDISCrossSection::GetPossibl } std::vector PythiaDISCrossSection::GetPossibleTargets() const { - return std::vector(target_types_.begin(), target_types_.end()); + return charm_xsec::ToVector(target_types_); } std::vector PythiaDISCrossSection::GetPossibleSignaturesFromParents(siren::dataclasses::ParticleType primary_type, siren::dataclasses::ParticleType target_type) const { - std::pair key(primary_type, target_type); - if(signatures_by_parent_types_.find(key) != signatures_by_parent_types_.end()) { - return signatures_by_parent_types_.at(key); - } - return std::vector(); + return charm_xsec::SignaturesForParents(signatures_by_parent_types_, primary_type, target_type); } std::vector PythiaDISCrossSection::DensityVariables() const { diff --git a/projects/interactions/private/QuarkDISFromSpline.cxx b/projects/interactions/private/QuarkDISFromSpline.cxx index 5fabdbf10..9d9ec20e2 100644 --- a/projects/interactions/private/QuarkDISFromSpline.cxx +++ b/projects/interactions/private/QuarkDISFromSpline.cxx @@ -18,6 +18,7 @@ #include // for splinetable #include "SIREN/interactions/CrossSection.h" // for CrossSection +#include "SIREN/interactions/CharmCrossSectionHelpers.h" // shared charm-DIS helpers #include "SIREN/dataclasses/InteractionRecord.h" // for Interactio... #include "SIREN/dataclasses/Particle.h" // for Particle #include "SIREN/utilities/Random.h" // for SIREN_random @@ -65,18 +66,12 @@ bool kinematicallyAllowed(double xi, double y, double E, double M, double m_lep) if (W2 <= (M + Mch) * (M + Mch)) return false; // Transverse-momentum balance: the exchanged q must have a real transverse - // component pqy in the lab frame. This is the same q-decomposition the sampler - // uses (SampleFinalState), so kinematicallyAllowed is the single predicate - // shared by the sampler proposal loops and by - // DifferentialCrossSection/FinalStateProbability. Without this check the - // density (dxs/txs) would be nonzero on points the sampler rejects (pqy^2 < 0), - // breaking Sample==Density closure and silently biasing low-Bjorken-x events. - // - // Primary is a neutrino here (InitializeSignatures enforces isNeutrino), so - // m1 = 0 and |p1| = E. Using P1 = E (massless primary) to match the sampler. - // pqy^2 = momq^2 - pqx^2 with: - // pqx = (m_lep^2 + 2 P1^2 + Q2 + 2 E^2 (y-1)) / (2 P1) - // momq^2 = P1^2 + Q2 + E^2 (y^2 - 1) + // component pqy. Same q-decomposition the sampler uses, so this is the single + // predicate shared by the sampler and by DifferentialCrossSection; without it + // the density would be nonzero on points the sampler rejects (closure break). + // Massless primary (InitializeSignatures enforces isNeutrino): m1=0, |p1|=E. + // pqy^2 = momq^2 - pqx^2, + // pqx = (m_lep^2 + 2 P1^2 + Q2 + 2 E^2 (y-1))/(2 P1), momq^2 = P1^2 + Q2 + E^2(y^2-1) const double P1 = E; const double pqx = (m_lep * m_lep + 2.0 * P1 * P1 + Q2 + 2.0 * E * E * (y - 1.0)) / (2.0 * P1); const double momq2 = P1 * P1 + Q2 + E * E * (y * y - 1.0); @@ -148,15 +143,7 @@ QuarkDISFromSpline::QuarkDISFromSpline(std::string differential_filename, std::s } void QuarkDISFromSpline::SetUnits(std::string units) { - std::transform(units.begin(), units.end(), units.begin(), - [](unsigned char c){ return std::tolower(c); }); - if(units == "cm") { - unit = 1.0; - } else if(units == "m") { - unit = 10000.0; - } else { - throw std::runtime_error("Cross section units not supported!"); - } + unit = charm_xsec::UnitForString(units); } void QuarkDISFromSpline::SetInteractionType(int interaction) { @@ -212,31 +199,7 @@ void QuarkDISFromSpline::LoadFromMemory(std::vector & differential_data, s } double QuarkDISFromSpline::GetLeptonMass(siren::dataclasses::ParticleType lepton_type) { - int32_t lepton_number = std::abs(static_cast(lepton_type)); - double lepton_mass; - switch(lepton_number) { - case 11: - lepton_mass = siren::utilities::Constants::electronMass; - break; - case 13: - lepton_mass = siren::utilities::Constants::muonMass; - break; - case 15: - lepton_mass = siren::utilities::Constants::tauMass; - break; - case 12: - lepton_mass = 0; - break; - case 14: - lepton_mass = 0; - break; - case 16: - lepton_mass = 0; - break; - default: - throw std::runtime_error("Unknown lepton type!"); - } - return lepton_mass; + return charm_xsec::GetLeptonMass(lepton_type); } double QuarkDISFromSpline::getHadronMass(siren::dataclasses::ParticleType hadron_type) { @@ -353,23 +316,8 @@ void QuarkDISFromSpline::InitializeSignatures() { throw std::runtime_error("This DIS implementation only supports neutrinos as primaries!"); } // first push back the charged lepton product - siren::dataclasses::ParticleType charged_lepton_product = siren::dataclasses::ParticleType::unknown; + siren::dataclasses::ParticleType charged_lepton_product = charm_xsec::ChargedLeptonProduct(primary_type); siren::dataclasses::ParticleType neutral_lepton_product = primary_type; - if(primary_type == siren::dataclasses::ParticleType::NuE) { - charged_lepton_product = siren::dataclasses::ParticleType::EMinus; - } else if(primary_type == siren::dataclasses::ParticleType::NuEBar) { - charged_lepton_product = siren::dataclasses::ParticleType::EPlus; - } else if(primary_type == siren::dataclasses::ParticleType::NuMu) { - charged_lepton_product = siren::dataclasses::ParticleType::MuMinus; - } else if(primary_type == siren::dataclasses::ParticleType::NuMuBar) { - charged_lepton_product = siren::dataclasses::ParticleType::MuPlus; - } else if(primary_type == siren::dataclasses::ParticleType::NuTau) { - charged_lepton_product = siren::dataclasses::ParticleType::TauMinus; - } else if(primary_type == siren::dataclasses::ParticleType::NuTauBar) { - charged_lepton_product = siren::dataclasses::ParticleType::TauPlus; - } else { - throw std::runtime_error("InitializeSignatures: Unkown parent neutrino type!"); - } if(interaction_type_ == 1) { signature.secondary_types.push_back(charged_lepton_product); } else if(interaction_type_ == 2) { @@ -566,9 +514,8 @@ double QuarkDISFromSpline::DifferentialCrossSection(dataclasses::InteractionReco double QuarkDISFromSpline::DifferentialCrossSection(double energy, double xi, double y, double secondary_lepton_mass, double Q2) const { double log_energy = log10(energy); - // Out of spline SUPPORT -> raise, never silently return 0: a silent zero on a - // genuinely sampled event would bias that event's physical density (and hence - // its weight) to zero. + // Out of spline support -> raise, never silently return 0: a silent zero on a + // genuinely sampled event would bias that event's weight to zero. if (log_energy < differential_cross_section_.lower_extent(0) || log_energy > differential_cross_section_.upper_extent(0)) { throw std::runtime_error("QuarkDISFromSpline: energy " + std::to_string(energy) @@ -802,19 +749,10 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR double Q2 = slowRescalingQ2(final_xi, final_y, E1_lab, target_mass_, siren::utilities::Constants::charmMass); - // Closed form for the exchanged q decomposition. A uniform momentum rescaling - // cannot be used to recompute Q2 here: slowRescalingQ2 holds the target/charm - // masses fixed, so Q2 is not homogeneous of degree 2 under the scaling. - // - // p1x_lab is the 3-momentum MAGNITUDE P1 = |p1_lab| (not an x-component). - // The naive expressions - // pqx = (m1^2 + m3^2 + 2 P1^2 + Q2 + 2 E1^2 (y-1)) / (2 P1) - // momq^2 = m1^2 + P1^2 + Q2 + E1^2 (y^2 - 1) - // lose precision because both carry dominant ~E1^2 terms that nearly cancel - // in pqy^2 = momq^2 - pqx^2. Substituting P1^2 = E1^2 - m1^2 cancels those - // terms analytically before the numeric evaluation: - // pqx = (m3^2 - m1^2 + Q2 + 2 E1^2 y) / (2 P1) - // momq^2 = Q2 + E1^2 y^2 + // Closed form for the exchanged q. p1x_lab is the 3-momentum MAGNITUDE + // P1 = |p1_lab|. Substituting P1^2 = E1^2 - m1^2 cancels the dominant ~E1^2 + // terms analytically (the naive pqx/momq^2 lose precision in pqy^2 = momq^2-pqx^2): + // pqx = (m3^2 - m1^2 + Q2 + 2 E1^2 y)/(2 P1), momq^2 = Q2 + E1^2 y^2 double p1x_lab = std::sqrt(p1_lab.px() * p1_lab.px() + p1_lab.py() * p1_lab.py() + p1_lab.pz() * p1_lab.pz()); double pqx_lab = (m3 * m3 - m1 * m1 + Q2 + 2.0 * E1_lab * E1_lab * final_y) / (2.0 * p1x_lab); double momq2_lab = Q2 + E1_lab * E1_lab * final_y * final_y; @@ -888,17 +826,9 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR p4X = p_spectator + p4_lab - p4CH; } while (p4X.dot(p4X) < 0); - // Save final state kinematics. - // - // NOTE: the sampled momenta are written into the record's - // SecondaryParticleRecord vector (record.GetSecondaryParticleRecords()), NOT - // directly into an InteractionRecord's secondary_momenta. To obtain a finalized - // InteractionRecord with populated secondary_momenta, the caller must run - // CrossSectionDistributionRecord::Finalize (pybind: cdr.finalize(ir)) into an - // output record whose signature is set. Building an InteractionRecord by hand - // with empty/zero secondary_momenta and feeding it back to - // DifferentialCrossSection makes the primary-momentum Q2 path compute Q2 <= 0, - // forcing the stored-(xi,y) fallback branch. + // Save final state kinematics into the record's SecondaryParticleRecord vector + // (not directly into secondary_momenta); the caller runs + // CrossSectionDistributionRecord::Finalize to populate a finalized record. std::vector & secondaries = record.GetSecondaryParticleRecords(); siren::dataclasses::SecondaryParticleRecord & lepton = secondaries[lepton_index]; siren::dataclasses::SecondaryParticleRecord & hadron = secondaries[hadron_index]; @@ -919,38 +849,13 @@ void QuarkDISFromSpline::SampleFinalState(dataclasses::CrossSectionDistributionR } double QuarkDISFromSpline::FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) const { - // D0:D+/-:Ds = 0.60:0.23:0.15, renormalized to sum to 1.0. The Lambda_c - // channel (~0.02) is not modeled, so its fraction is redistributed into the - // implemented D species; otherwise summing TotalCrossSection over the three - // registered D signatures would recover only 0.98 * sigma_inclusive and - // under-count the charm rate by ~2%. - if (secondary == siren::dataclasses::Particle::ParticleType::D0 || secondary == siren::dataclasses::Particle::ParticleType::D0Bar) { - return 0.6 / 0.98; - } else if (secondary == siren::dataclasses::Particle::ParticleType::DPlus || secondary == siren::dataclasses::Particle::ParticleType::DMinus) { - return 0.23 / 0.98; - } else if (secondary == siren::dataclasses::Particle::ParticleType::DsPlus || secondary == siren::dataclasses::Particle::ParticleType::DsMinus) { - return 0.15 / 0.98; - } - return 0; + return charm_xsec::FragmentationFraction(secondary); } -// UNBIASED-ONLY CONTRACT: SampleFinalState samples (xi,y) AND an independent -// fragmentation z (the inverse-CDF draw) and uniform azimuth phi that set the -// D-meson momentum. FinalStateProbability / DifferentialCrossSection account for -// (xi,y) only; the z and phi factors are NOT included here. They cancel exactly -// in the weight ratio ONLY when the same cross-section object provides both the -// injection and physical densities and no biased phase-space channel is installed -// on the D kinematics. Biasing the D kinematics is NOT supported and would produce -// incorrect weights. -// -// NORMALIZATION CONTRACT: FinalStateProbability = dxs/txs is a normalized -// kinematic density ONLY if the external 1-D total-xs spline (txs) equals the -// integral of the differential spline (dxs) over the SAME truncated slow-rescaling -// domain: xi in [xiMin(E),1], y in [yMin(E),yMax(E)] with identical charm-threshold -// (Q2 = 2 M E xi y - m_c^2 > 0), W2 > (M + M_D0)^2, Q2 >= minimum_Q2_ cuts and the -// same TARGETMASS. If the upstream total spline integrates a different domain the -// density is mis-normalized. The fragmentation fraction is applied inside -// TotalCrossSection(record) so per-species txs carries the D-species branching. +// FinalStateProbability = dxs/txs. Normalized only if the external total-xs spline +// integrates the same truncated (xi,y) domain (charm-threshold, W2, Q2>=minimum_Q2_, +// TARGETMASS) as the differential spline. See the contract blocks in the header. +// Fragmentation fraction is applied inside TotalCrossSection(record). double QuarkDISFromSpline::FinalStateProbability(dataclasses::InteractionRecord const & interaction) const { // first compute the differential and total cross section double dxs = DifferentialCrossSection(interaction); @@ -964,11 +869,11 @@ double QuarkDISFromSpline::FinalStateProbability(dataclasses::InteractionRecord } std::vector QuarkDISFromSpline::GetPossiblePrimaries() const { - return std::vector(primary_types_.begin(), primary_types_.end()); + return charm_xsec::ToVector(primary_types_); } std::vector QuarkDISFromSpline::GetPossibleTargetsFromPrimary(siren::dataclasses::ParticleType primary_type) const { - return std::vector(target_types_.begin(), target_types_.end()); + return charm_xsec::ToVector(target_types_); } std::vector QuarkDISFromSpline::GetPossibleSignatures() const { @@ -976,22 +881,16 @@ std::vector QuarkDISFromSpline::GetPossibleSi } std::vector QuarkDISFromSpline::GetPossibleTargets() const { - return std::vector(target_types_.begin(), target_types_.end()); + return charm_xsec::ToVector(target_types_); } std::vector QuarkDISFromSpline::GetPossibleSignaturesFromParents(siren::dataclasses::ParticleType primary_type, siren::dataclasses::ParticleType target_type) const { - std::pair key(primary_type, target_type); - if(signatures_by_parent_types_.find(key) != signatures_by_parent_types_.end()) { - return signatures_by_parent_types_.at(key); - } else { - return std::vector(); - } + return charm_xsec::SignaturesForParents(signatures_by_parent_types_, primary_type, target_type); } -// UNBIASED-ONLY CONTRACT: the density covers only (xi,y); the independently-sampled -// fragmentation z and azimuth phi (set in SampleFinalState) are omitted. They cancel -// in the weight ratio only in the unbiased configuration (same cross-section object on -// both sides, no biased D-kinematics channel). Biasing D kinematics is NOT supported. +// Density covers only (xi,y); the independently-sampled fragmentation z and +// azimuth phi cancel in the weight ratio only in the unbiased configuration. +// See the UNBIASED-ONLY CONTRACT in the header. Biasing D kinematics is unsupported. std::vector QuarkDISFromSpline::DensityVariables() const { return std::vector{"Bjorken xi", "Bjorken y"}; } diff --git a/projects/interactions/private/test/CharmDISClosure_TEST.cxx b/projects/interactions/private/test/CharmDISClosure_TEST.cxx index 784a49959..53eb316f4 100644 --- a/projects/interactions/private/test/CharmDISClosure_TEST.cxx +++ b/projects/interactions/private/test/CharmDISClosure_TEST.cxx @@ -130,15 +130,6 @@ double gen_path(MockCharmXS const & xs, ParticleType primary, ParticleType targe } // namespace -// The base-class default must sum TotalCrossSection over the registered -// signatures: three D-type signatures sharing sigma -> 3*sigma. -TEST(CharmDISClosure, BaseDefaultSumsOverSignatures) { - MockCharmXS xs(/*ff=*/false, /*override=*/false); - const double E = 100.0; - const double s = MockCharmXS::sigma_inclusive(E); - EXPECT_NEAR(gen_path(xs, ParticleType::NuMu, ParticleType::PPlus, E), 3.0 * s, 1e-50); -} - // Reproduces the f1751c6b bug: the override makes the generation side report 1x // while the physical side reports 3x -> closure broken by a factor of 3. TEST(CharmDISClosure, OverrideBreaksClosureByFactorThree) { diff --git a/projects/interactions/private/test/CharmDecayTestHelpers.h b/projects/interactions/private/test/CharmDecayTestHelpers.h new file mode 100644 index 000000000..225beda44 --- /dev/null +++ b/projects/interactions/private/test/CharmDecayTestHelpers.h @@ -0,0 +1,65 @@ +#pragma once +#ifndef SIREN_CharmDecayTestHelpers_H +#define SIREN_CharmDecayTestHelpers_H + +// Shared numeric oracle + record helpers for the CharmMesonDecay closure tests. +// Both decay test files include this so the V-A angle-average cross-check and +// the q^2 reconstruction stay in one place. + +#include + +#include +#include + +#include "SIREN/dataclasses/InteractionRecord.h" + +namespace siren { +namespace interactions { +namespace charm_decay_test { + +// q^2 = (p_D - p_K)^2 from a finalized record. +inline double reconstruct_q2(const siren::dataclasses::InteractionRecord & rec) { + double qE = rec.primary_momentum[0] - rec.secondary_momenta[0][0]; + double qpx = rec.primary_momentum[1] - rec.secondary_momenta[0][1]; + double qpy = rec.primary_momentum[2] - rec.secondary_momenta[0][2]; + double qpz = rec.primary_momentum[3] - rec.secondary_momenta[0][3]; + return qE * qE - qpx * qpx - qpy * qpy - qpz * qpz; +} + +// Independent numeric quadrature of the clamped V-A weight, evaluated with the +// same rk::P4 boosts SampleFinalState uses. Cross-checks the closed-form +// charm_decay::VAWeightAngleAverage used by the weighting code. +inline double numericVAWeightAngleAverage(double mD, double mK, double ml, double m23) { + double mnu = 0.0; + double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) + * (mD + mK - m23) * (mD - mK + m23)) / mD; + double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) + * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; + if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; + double wtMEmax = std::min(std::pow(mD, 4) / 16.0, + mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); + rk::P4 p4m23(geom3::Vector3(0.0, 0.0, -p1Abs), m23); + rk::Boost boost = p4m23.labBoost(); + rk::P4 p4K(geom3::Vector3(0.0, 0.0, p1Abs), mK); + const int N = 20000; // even -> composite Simpson + double h = 2.0 / N, sum = 0.0; + for (int i = 0; i <= N; ++i) { + double c = -1.0 + i * h; + double s = std::sqrt(std::max(0.0, 1.0 - c * c)); + geom3::Vector3 dir(s, 0.0, c); + rk::P4 p4l = rk::P4(p23Abs * dir, ml).boost(boost); + rk::P4 p4nu = rk::P4(-p23Abs * dir, mnu).boost(boost); + double w = mD * p4l.e() * p4nu.dot(p4K); + if (w < 0.0) w = 0.0; + if (w > wtMEmax) w = wtMEmax; + double wgt = (i == 0 || i == N) ? 1.0 : ((i % 2) ? 4.0 : 2.0); + sum += wgt * w; + } + return 0.5 * (sum * h / 3.0); +} + +} // namespace charm_decay_test +} // namespace interactions +} // namespace siren + +#endif // SIREN_CharmDecayTestHelpers_H diff --git a/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx b/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx index ebd8d05fa..c625334ef 100644 --- a/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx +++ b/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx @@ -38,15 +38,20 @@ #include #include "SIREN/interactions/CharmMesonDecay3Body.h" +#include "SIREN/interactions/CharmDecayKinematics.h" #include "SIREN/dataclasses/Particle.h" #include "SIREN/dataclasses/InteractionRecord.h" #include "SIREN/dataclasses/InteractionSignature.h" #include "SIREN/utilities/Random.h" #include "SIREN/utilities/Constants.h" +#include "CharmDecayTestHelpers.h" + using namespace siren::utilities; using namespace siren::interactions; using namespace siren::dataclasses; +using siren::interactions::charm_decay_test::reconstruct_q2; +using siren::interactions::charm_decay_test::numericVAWeightAngleAverage; namespace { @@ -67,15 +72,6 @@ InteractionRecord make_semilep_record(const InteractionSignature & sig, return rec; } -// q^2 = (p_D - p_K)^2 from a finalized record. -double reconstruct_q2(const InteractionRecord & rec) { - double qE = rec.primary_momentum[0] - rec.secondary_momenta[0][0]; - double qpx = rec.primary_momentum[1] - rec.secondary_momenta[0][1]; - double qpy = rec.primary_momentum[2] - rec.secondary_momenta[0][2]; - double qpz = rec.primary_momentum[3] - rec.secondary_momenta[0][3]; - return qE * qE - qpx * qpx - qpy * qpy - qpz * qpz; -} - } // namespace // --- Test 1: exact 4-momentum conservation ---------------------------------- @@ -345,50 +341,17 @@ TEST(CharmMesonDecay3Body, FinalStateProbabilityClosure) { // --- Analytic angle-average matches a numeric quadrature oracle ------------ // -// The weighting code uses the closed-form CharmMesonDecay3Body:: -// VAWeightAngleAverage. The closed form must reproduce a high-resolution numeric -// quadrature of the identical clamped V-A weight, evaluated with the same rk::P4 -// boosts SampleFinalState uses. -namespace { -double numericVAWeightAngleAverage3B(double mD, double mK, double ml, double m23) { - double mnu = 0.0; - double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) - * (mD + mK - m23) * (mD - mK + m23)) / mD; - double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) - * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; - if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; - double wtMEmax = std::min(std::pow(mD, 4) / 16.0, - mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); - rk::P4 p4m23(geom3::Vector3(0.0, 0.0, -p1Abs), m23); - rk::Boost boost = p4m23.labBoost(); - rk::P4 p4K(geom3::Vector3(0.0, 0.0, p1Abs), mK); - const int N = 20000; - double h = 2.0 / N, sum = 0.0; - for (int i = 0; i <= N; ++i) { - double c = -1.0 + i * h; - double s = std::sqrt(std::max(0.0, 1.0 - c * c)); - geom3::Vector3 dir(s, 0.0, c); - rk::P4 p4l = rk::P4(p23Abs * dir, ml).boost(boost); - rk::P4 p4nu = rk::P4(-p23Abs * dir, mnu).boost(boost); - double w = mD * p4l.e() * p4nu.dot(p4K); - if (w < 0.0) w = 0.0; - if (w > wtMEmax) w = wtMEmax; - double wgt = (i == 0 || i == N) ? 1.0 : ((i % 2) ? 4.0 : 2.0); - sum += wgt * w; - } - return 0.5 * (sum * h / 3.0); -} -} // namespace - +// charm_decay::VAWeightAngleAverage (used by the weighting code) must reproduce +// a numeric quadrature of the identical clamped V-A weight +// (numericVAWeightAngleAverage, in CharmDecayTestHelpers.h). TEST(CharmMesonDecay3Body, VAWeightAngleAverageMatchesNumericReference) { - CharmMesonDecay3Body d0(ParticleType::D0), dp(ParticleType::DPlus); - struct Case { CharmMesonDecay3Body* dec; double mD; double mK; double ml; }; + struct Case { double mD; double mK; double ml; }; std::vector cases = { - {&d0, Constants::D0Mass, Constants::KMinusMass, Constants::electronMass}, - {&d0, Constants::D0Mass, Constants::KMinusMass, Constants::muonMass}, - {&d0, Constants::D0Mass, Constants::KPrimePlusMass, Constants::electronMass}, - {&dp, Constants::DPlusMass, Constants::K0Mass, Constants::electronMass}, - {&dp, Constants::DPlusMass, Constants::KPrimePlusMass, Constants::muonMass}, + {Constants::D0Mass, Constants::KMinusMass, Constants::electronMass}, + {Constants::D0Mass, Constants::KMinusMass, Constants::muonMass}, + {Constants::D0Mass, Constants::KPrimePlusMass, Constants::electronMass}, + {Constants::DPlusMass, Constants::K0Mass, Constants::electronMass}, + {Constants::DPlusMass, Constants::KPrimePlusMass, Constants::muonMass}, }; for (auto & cs : cases) { double m23Min = cs.ml; @@ -396,8 +359,8 @@ TEST(CharmMesonDecay3Body, VAWeightAngleAverageMatchesNumericReference) { const int NG = 40; for (int g = 1; g < NG; ++g) { double m23 = m23Min + (m23Max - m23Min) * (double)g / NG; - double ana = cs.dec->VAWeightAngleAverage(cs.mD, cs.mK, cs.ml, m23); - double num = numericVAWeightAngleAverage3B(cs.mD, cs.mK, cs.ml, m23); + double ana = charm_decay::VAWeightAngleAverage(cs.mD, cs.mK, cs.ml, m23); + double num = numericVAWeightAngleAverage(cs.mD, cs.mK, cs.ml, m23); double tol = 1e-4 * std::abs(num) + 1e-12; EXPECT_NEAR(ana, num, tol) << "mD=" << cs.mD << " mK=" << cs.mK << " ml=" << cs.ml << " m23=" << m23; diff --git a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx index 6b653de55..8f47eaa21 100644 --- a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx +++ b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx @@ -35,13 +35,18 @@ #include "SIREN/utilities/Constants.h" #include "SIREN/utilities/Random.h" #include "SIREN/interactions/CharmMesonDecay.h" +#include "SIREN/interactions/CharmDecayKinematics.h" #include "SIREN/dataclasses/Particle.h" #include "SIREN/dataclasses/InteractionRecord.h" #include "SIREN/dataclasses/InteractionSignature.h" +#include "CharmDecayTestHelpers.h" + using namespace siren::utilities; using namespace siren::interactions; using namespace siren::dataclasses; +using siren::interactions::charm_decay_test::reconstruct_q2; +using siren::interactions::charm_decay_test::numericVAWeightAngleAverage; // --- Test 1: Interpolator1D with known linear CDF ------------------------ @@ -181,15 +186,6 @@ TEST(Interpolator1D, DMesonDecayCDF) { // --- Test 3: SampleFinalState q^2 closes with FinalStateProbability ------- namespace { -// Reconstruct q^2 = (p_D - p_K)^2 from a finalized record. -double reconstruct_q2(const InteractionRecord & rec) { - double qE = rec.primary_momentum[0] - rec.secondary_momenta[0][0]; - double qpx = rec.primary_momentum[1] - rec.secondary_momenta[0][1]; - double qpy = rec.primary_momentum[2] - rec.secondary_momenta[0][2]; - double qpz = rec.primary_momentum[3] - rec.secondary_momenta[0][3]; - return qE * qE - qpx * qpx - qpy * qpy - qpz * qpz; -} - // Build a record at a given q^2 in the D rest frame with hadron mass mK so that // FinalStateProbability can be evaluated on a single mixture component. InteractionRecord make_record_at_q2(const InteractionSignature & sig, @@ -351,50 +347,18 @@ TEST(CharmMesonDecay, UnsupportedSignaturesThrow) { // --- Test 5: analytic angle-average matches a numeric quadrature oracle ----- // // The weighting code (FinalStateProbability via SampledQ2Density) uses the -// closed-form CharmMesonDecay::VAWeightAngleAverage. The closed form must -// reproduce a high-resolution numeric quadrature of the identical clamped V-A -// weight, evaluated with the same rk::P4 boosts SampleFinalState uses. -namespace { -double numericVAWeightAngleAverage(double mD, double mK, double ml, double m23) { - double mnu = 0.0; - double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) - * (mD + mK - m23) * (mD - mK + m23)) / mD; - double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) - * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; - if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; - double wtMEmax = std::min(std::pow(mD, 4) / 16.0, - mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); - rk::P4 p4m23(geom3::Vector3(0.0, 0.0, -p1Abs), m23); - rk::Boost boost = p4m23.labBoost(); - rk::P4 p4K(geom3::Vector3(0.0, 0.0, p1Abs), mK); - const int N = 20000; // even -> composite Simpson - double h = 2.0 / N, sum = 0.0; - for (int i = 0; i <= N; ++i) { - double c = -1.0 + i * h; - double s = std::sqrt(std::max(0.0, 1.0 - c * c)); - geom3::Vector3 dir(s, 0.0, c); - rk::P4 p4l = rk::P4(p23Abs * dir, ml).boost(boost); - rk::P4 p4nu = rk::P4(-p23Abs * dir, mnu).boost(boost); - double w = mD * p4l.e() * p4nu.dot(p4K); - if (w < 0.0) w = 0.0; - if (w > wtMEmax) w = wtMEmax; - double wgt = (i == 0 || i == N) ? 1.0 : ((i % 2) ? 4.0 : 2.0); - sum += wgt * w; - } - return 0.5 * (sum * h / 3.0); -} -} // namespace - +// closed-form charm_decay::VAWeightAngleAverage. It must reproduce a numeric +// quadrature of the identical clamped V-A weight (numericVAWeightAngleAverage, +// in CharmDecayTestHelpers.h), evaluated with the same rk::P4 boosts. TEST(CharmMesonDecay, VAWeightAngleAverageMatchesNumericReference) { - CharmMesonDecay d0(ParticleType::D0), dp(ParticleType::DPlus); - struct Case { CharmMesonDecay* dec; double mD; double mK; double ml; }; + struct Case { double mD; double mK; double ml; }; std::vector cases = { - {&d0, Constants::D0Mass, Constants::KMinusMass, Constants::electronMass}, - {&d0, Constants::D0Mass, Constants::KMinusMass, Constants::muonMass}, - {&d0, Constants::D0Mass, Constants::KPrimePlusMass, Constants::electronMass}, - {&dp, Constants::DPlusMass, Constants::K0Mass, Constants::electronMass}, - {&dp, Constants::DPlusMass, Constants::K0Mass, Constants::muonMass}, - {&dp, Constants::DPlusMass, Constants::KPrimePlusMass, Constants::muonMass}, + {Constants::D0Mass, Constants::KMinusMass, Constants::electronMass}, + {Constants::D0Mass, Constants::KMinusMass, Constants::muonMass}, + {Constants::D0Mass, Constants::KPrimePlusMass, Constants::electronMass}, + {Constants::DPlusMass, Constants::K0Mass, Constants::electronMass}, + {Constants::DPlusMass, Constants::K0Mass, Constants::muonMass}, + {Constants::DPlusMass, Constants::KPrimePlusMass, Constants::muonMass}, }; for (auto & cs : cases) { double m23Min = cs.ml; @@ -402,7 +366,7 @@ TEST(CharmMesonDecay, VAWeightAngleAverageMatchesNumericReference) { const int NG = 40; for (int g = 1; g < NG; ++g) { double m23 = m23Min + (m23Max - m23Min) * (double)g / NG; - double ana = cs.dec->VAWeightAngleAverage(cs.mD, cs.mK, cs.ml, m23); + double ana = charm_decay::VAWeightAngleAverage(cs.mD, cs.mK, cs.ml, m23); double num = numericVAWeightAngleAverage(cs.mD, cs.mK, cs.ml, m23); // Tolerance is quadrature-limited (Simpson degrades to O(h^2) at the // clamp kinks); 1e-4 relative is far tighter than any real algebra bug. diff --git a/projects/interactions/public/SIREN/interactions/CharmCrossSectionHelpers.h b/projects/interactions/public/SIREN/interactions/CharmCrossSectionHelpers.h new file mode 100644 index 000000000..c1b0de7c9 --- /dev/null +++ b/projects/interactions/public/SIREN/interactions/CharmCrossSectionHelpers.h @@ -0,0 +1,110 @@ +#pragma once +#ifndef SIREN_CharmCrossSectionHelpers_H +#define SIREN_CharmCrossSectionHelpers_H + +// Shared, header-only helpers for the charm-DIS cross sections +// (QuarkDISFromSpline and PythiaDISCrossSection). These two classes are +// intentionally independent (no common base); this file holds only the byte- +// identical pieces, moved verbatim, so the values and behavior are unchanged. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "SIREN/dataclasses/Particle.h" +#include "SIREN/dataclasses/InteractionSignature.h" +#include "SIREN/utilities/Constants.h" + +namespace siren { +namespace interactions { +namespace charm_xsec { + +// D0:D+/-:Ds = 0.60:0.23:0.15, renormalized to sum to 1.0. The Lambda_c channel +// (~0.02) is not modeled, so its fraction is redistributed into the implemented +// D species (each /0.98); otherwise summing TotalCrossSection over the three +// registered D signatures recovers only 0.98*sigma_inclusive and under-counts +// charm production. Values are load-bearing for interaction-depth normalization. +inline double FragmentationFraction(siren::dataclasses::Particle::ParticleType secondary) { + if (secondary == siren::dataclasses::Particle::ParticleType::D0 || secondary == siren::dataclasses::Particle::ParticleType::D0Bar) { + return 0.6 / 0.98; + } else if (secondary == siren::dataclasses::Particle::ParticleType::DPlus || secondary == siren::dataclasses::Particle::ParticleType::DMinus) { + return 0.23 / 0.98; + } else if (secondary == siren::dataclasses::Particle::ParticleType::DsPlus || secondary == siren::dataclasses::Particle::ParticleType::DsMinus) { + return 0.15 / 0.98; + } + return 0; +} + +// Map a neutrino primary to its charged-lepton product (CC signature building). +// Throws on a non-neutrino input. The interaction-type policy (which classes +// accept CC/NC) stays in the caller; only this flavor map is shared. +inline siren::dataclasses::ParticleType ChargedLeptonProduct(siren::dataclasses::ParticleType primary_type) { + if(primary_type == siren::dataclasses::ParticleType::NuE) { + return siren::dataclasses::ParticleType::EMinus; + } else if(primary_type == siren::dataclasses::ParticleType::NuEBar) { + return siren::dataclasses::ParticleType::EPlus; + } else if(primary_type == siren::dataclasses::ParticleType::NuMu) { + return siren::dataclasses::ParticleType::MuMinus; + } else if(primary_type == siren::dataclasses::ParticleType::NuMuBar) { + return siren::dataclasses::ParticleType::MuPlus; + } else if(primary_type == siren::dataclasses::ParticleType::NuTau) { + return siren::dataclasses::ParticleType::TauMinus; + } else if(primary_type == siren::dataclasses::ParticleType::NuTauBar) { + return siren::dataclasses::ParticleType::TauPlus; + } else { + throw std::runtime_error("InitializeSignatures: Unknown parent neutrino type!"); + } +} + +// Lepton mass by |PDG| (neutrinos -> 0); throws on a non-lepton input. +inline double GetLeptonMass(siren::dataclasses::ParticleType lepton_type) { + int32_t lepton_number = std::abs(static_cast(lepton_type)); + switch(lepton_number) { + case 11: return siren::utilities::Constants::electronMass; + case 13: return siren::utilities::Constants::muonMass; + case 15: return siren::utilities::Constants::tauMass; + case 12: case 14: case 16: return 0; + default: throw std::runtime_error("Unknown lepton type!"); + } +} + +// Cross-section unit multiplier: "cm" -> 1, "m" -> 10000 (cm^2 internal). +inline double UnitForString(std::string units) { + std::transform(units.begin(), units.end(), units.begin(), + [](unsigned char c){ return std::tolower(c); }); + if(units == "cm") { + return 1.0; + } else if(units == "m") { + return 10000.0; + } else { + throw std::runtime_error("Cross section units not supported!"); + } +} + +inline std::vector ToVector(std::set const & types) { + return std::vector(types.begin(), types.end()); +} + +inline std::vector SignaturesForParents( + std::map, std::vector> const & by_parent, + siren::dataclasses::ParticleType primary_type, + siren::dataclasses::ParticleType target_type) { + std::pair key(primary_type, target_type); + auto it = by_parent.find(key); + if(it != by_parent.end()) { + return it->second; + } + return std::vector(); +} + +} // namespace charm_xsec +} // namespace interactions +} // namespace siren + +#endif // SIREN_CharmCrossSectionHelpers_H diff --git a/projects/interactions/public/SIREN/interactions/CharmDecayKinematics.h b/projects/interactions/public/SIREN/interactions/CharmDecayKinematics.h new file mode 100644 index 000000000..06bf2485b --- /dev/null +++ b/projects/interactions/public/SIREN/interactions/CharmDecayKinematics.h @@ -0,0 +1,209 @@ +#pragma once +#ifndef SIREN_CharmDecayKinematics_H +#define SIREN_CharmDecayKinematics_H + +// Shared closure kinematics for the charm-meson semileptonic decay samplers. +// +// CharmMesonDecay and CharmMesonDecay3Body are independent classes (no shared +// base), but their SampleFinalState <-> FinalStateProbability closure relies on +// the same q^2 density. These inline free helpers are the single source of that +// math so both classes stay byte-for-byte consistent. +// +// Closure rationale (authoritative copy): FinalStateProbability must be the +// normalized q^2 density that SampleFinalState produces, because the Weighter +// consumes it as the physical final-state density. The sampler draws +// m23 = sqrt(q^2) flat with accept-reject on the phase-space weight +// p1Abs*p23Abs, then (for D+/D0) accept-rejects on the V-A weight +// wtME = mD*E_l*(p_nu . p_K). The accepted m23 density is therefore proportional +// to p1Abs(m23)*p23Abs(m23)*_angle, and the q^2 density carries the +// Jacobian dm23/dq^2 = 1/(2 m23). SampledQ2Density reproduces exactly this; the +// per-event numerator and the normalization integral both call it, so +// Sample == Density by construction. The form-factor DifferentialDecayWidth is a +// separate physical quantity the sampler never used and is NOT the closure +// density. + +#include +#include +#include + +#include "SIREN/dataclasses/Particle.h" +#include "SIREN/utilities/Constants.h" +#include "SIREN/utilities/Integration.h" + +namespace siren { +namespace interactions { +namespace charm_decay { + +inline double particleMass(siren::dataclasses::ParticleType particle) { + switch(particle){ + case siren::dataclasses::ParticleType::D0: + return( siren::utilities::Constants::D0Mass); + case siren::dataclasses::ParticleType::D0Bar: + return( siren::utilities::Constants::D0Mass); + case siren::dataclasses::ParticleType::DPlus: + return( siren::utilities::Constants::DPlusMass); + case siren::dataclasses::ParticleType::DMinus: + return( siren::utilities::Constants::DPlusMass); + case siren::dataclasses::ParticleType::K0: + return( siren::utilities::Constants::K0Mass); + case siren::dataclasses::ParticleType::K0Bar: + return( siren::utilities::Constants::K0Mass); + case siren::dataclasses::ParticleType::KPlus: + return( siren::utilities::Constants::KPlusMass); + case siren::dataclasses::ParticleType::KMinus: + return( siren::utilities::Constants::KMinusMass); + case siren::dataclasses::ParticleType::EPlus: + return( siren::utilities::Constants::electronMass ); + case siren::dataclasses::ParticleType::EMinus: + return( siren::utilities::Constants::electronMass ); + case siren::dataclasses::ParticleType::MuPlus: + return( siren::utilities::Constants::muonMass ); + case siren::dataclasses::ParticleType::MuMinus: + return( siren::utilities::Constants::muonMass ); + case siren::dataclasses::ParticleType::TauPlus: + return( siren::utilities::Constants::tauMass ); + case siren::dataclasses::ParticleType::TauMinus: + return( siren::utilities::Constants::tauMass ); + case siren::dataclasses::ParticleType::DsPlus: + return( siren::utilities::Constants::DsPlusMass ); + case siren::dataclasses::ParticleType::DsMinus: + return( siren::utilities::Constants::DsMinusMass ); + default: + return(0.0); + } +} + +// Single source of truth for the K*(892) mass shared by the sampler and the +// FinalStateProbability normalizer. +inline double KStarMass() { + return siren::utilities::Constants::KPrimePlusMass; +} + +// Integral over [lo, hi] of max(0, a2*c^2 + a1*c + a0). Closed form with a +// linear/constant fallback for a2 ~ 0 and numerically stable quadratic roots. +// Used to evaluate the angle-averaged V-A weight analytically. +inline double integratePositivePartQuadratic(double a2, double a1, double a0, + double lo, double hi) { + if (hi <= lo) return 0.0; + auto F = [&](double c) { return a2 * c * c * c / 3.0 + a1 * c * c / 2.0 + a0 * c; }; + auto seg = [&](double x0, double x1) -> double { + if (x1 <= x0) return 0.0; + return F(x1) - F(x0); + }; + double scale = std::abs(a1) + std::abs(a0) + 1.0; + if (std::abs(a2) < 1e-12 * scale) { + // Linear q(c) = a1*c + a0 (or constant if a1 ~ 0). + if (std::abs(a1) < 1e-300) return (a0 > 0.0) ? seg(lo, hi) : 0.0; + double root = -a0 / a1; + if (a1 > 0.0) return seg(std::max(lo, root), hi); // q > 0 for c > root + return seg(lo, std::min(hi, root)); // q > 0 for c < root + } + double disc = a1 * a1 - 4.0 * a2 * a0; + if (disc <= 0.0) { + // No real roots: q keeps the sign of a2 everywhere. + return (a2 > 0.0) ? seg(lo, hi) : 0.0; + } + double sq = std::sqrt(disc); + double qq = -0.5 * (a1 + ((a1 >= 0.0) ? sq : -sq)); // stable roots + double r1 = qq / a2; + double r2 = a0 / qq; + double rlo = std::min(r1, r2), rhi = std::max(r1, r2); + if (a2 > 0.0) { + // q > 0 outside [rlo, rhi]. + return seg(lo, std::min(hi, rlo)) + seg(std::max(lo, rhi), hi); + } + // q > 0 inside (rlo, rhi). + return seg(std::max(lo, rlo), std::min(hi, rhi)); +} + +// Analytic angle-average of the sampler's ACCEPTED V-A weight, +// (1/2) * integral_{-1}^{1} clamp(wtME(c), 0, wtMEmax) dc, +// with c = cos(theta_lepton) in the (l,nu) rest frame. After the boost to the D +// rest frame wtME = pref*(a0 + a1 c + a2 c^2) is an exact quadratic, so +// clamp(q,0,M) = max(0,q) - max(0,q-M) gives two closed-form positive-part +// integrals (no quadrature error). The unit tests cross-check this against a +// numeric quadrature of the identical clamped weight. +inline double VAWeightAngleAverage(double mD, double mK, double ml, double m23) { + double mnu = 0.0; + double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) + * (mD + mK - m23) * (mD - mK + m23)) / mD; + double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) + * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; + if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; + double E23 = std::sqrt(p1Abs * p1Abs + m23 * m23); + double bz = -p1Abs / E23; // (l,nu)-system velocity along -kaon axis + double gamma = E23 / m23; + double Elrest = std::sqrt(p23Abs * p23Abs + ml * ml); + double EK = std::sqrt(p1Abs * p1Abs + mK * mK); + // E_l = gamma (C + D c); (p_nu . p_K) = gamma p23Abs (A + B c). + double C = Elrest; + double D = bz * p23Abs; + double A = EK - p1Abs * bz; + double B = p1Abs - EK * bz; + double pref = mD * gamma * gamma * p23Abs; // > 0 + if (pref <= 0.0) return 0.0; + // wtME = pref * q(c), q(c) = (C + D c)(A + B c) = a0 + a1 c + a2 c^2. + double a0 = C * A; + double a1 = C * B + D * A; + double a2 = D * B; + // Same matrix-element ceiling as SampleFinalState (meMode == 22). + double wtMEmax = std::min(std::pow(mD, 4) / 16.0, + mD * (mD - mK - ml) * (mD - mK - mnu) * (mD - ml - mnu)); + double qmax = wtMEmax / pref; + double integral = integratePositivePartQuadratic(a2, a1, a0, -1.0, 1.0) + - integratePositivePartQuadratic(a2, a1, a0 - qmax, -1.0, 1.0); + return 0.5 * pref * integral; // average over c (measure width 2) +} + +// Unnormalized sampler density in q^2 for one hadron-mass component. apply_va +// toggles the V-A angle-average (D+/D0) vs pure phase space (Ds). +inline double SampledQ2Density(double mD, double mK, double ml, double q2, bool apply_va) { + double m23 = std::sqrt(q2); + double m23Max = mD - mK; + if (m23 <= ml || m23 >= m23Max) return 0.0; + double mnu = 0.0; + double p1Abs = 0.5 * std::sqrt((mD - mK - m23) * (mD + mK + m23) + * (mD + mK - m23) * (mD - mK + m23)) / mD; + double p23Abs = 0.5 * std::sqrt((m23 - ml - mnu) * (m23 + ml + mnu) + * (m23 + ml - mnu) * (m23 - ml + mnu)) / m23; + if (p1Abs <= 0.0 || p23Abs <= 0.0) return 0.0; + // Phase-space weight WITH the sampler's rejection ceiling: where + // p1Abs*p23Abs exceeds wtPSmax the sampler saturates (always accepts), so the + // accepted density is flat at wtPSmax. Reproduce the clip for exact closure. + double m23Min = ml + mnu; + double p1Max = 0.5 * std::sqrt((mD - mK - m23Min) * (mD + mK + m23Min) + * (mD + mK - m23Min) * (mD - mK + m23Min)) / mD; + double p23Max = 0.5 * std::sqrt((m23Max - ml - mnu) * (m23Max + ml + mnu) + * (m23Max + ml - mnu) * (m23Max - ml + mnu)) / m23Max; + double wtPSmax = 0.5 * p1Max * p23Max; + double wtPS = p1Abs * p23Abs; + if (wtPS > wtPSmax) wtPS = wtPSmax; + double me = apply_va ? VAWeightAngleAverage(mD, mK, ml, m23) : 1.0; + // Jacobian dm23/dq^2 = 1/(2 m23) converts the m23 density to a q^2 density. + return wtPS * me / (2.0 * m23); +} + +// Integral of SampledQ2Density over the allowed q^2 range, normalizing each +// mixture component to a proper pdf. Cached in norm_cache per (mD, mK, ml, +// apply_va) so the per-event FinalStateProbability call does not re-integrate. +inline double SampledQ2Normalization(double mD, double mK, double ml, bool apply_va, + std::map & norm_cache) { + double key = ((mD * 1e3 + mK) * 1e3 + ml) * 2.0 + (apply_va ? 1.0 : 0.0); + long ikey = (long)std::llround(key * 1e3); + auto it = norm_cache.find(ikey); + if (it != norm_cache.end()) return it->second; + double q2min = ml * ml; + double q2max = (mD - mK) * (mD - mK); + std::function integrand = [&](double q2) -> double { + return SampledQ2Density(mD, mK, ml, q2, apply_va); + }; + double n = siren::utilities::rombergIntegrate(integrand, q2min, q2max); + norm_cache[ikey] = n; + return n; +} + +} // namespace charm_decay +} // namespace interactions +} // namespace siren + +#endif // SIREN_CharmDecayKinematics_H diff --git a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h index ff640fdc9..d0b71b72a 100644 --- a/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h +++ b/projects/interactions/public/SIREN/interactions/CharmMesonDecay.h @@ -35,22 +35,13 @@ class CharmMesonDecay : public Decay { friend cereal::access; private: const std::set primary_types = {siren::dataclasses::Particle::ParticleType::D0, siren::dataclasses::Particle::ParticleType::DPlus, siren::dataclasses::Particle::ParticleType::DsPlus, siren::dataclasses::Particle::ParticleType::D0Bar, siren::dataclasses::Particle::ParticleType::DMinus, siren::dataclasses::Particle::ParticleType::DsMinus}; - // Shared closure helpers: SampleFinalState's density and FinalStateProbability - // both build on these, so Sample == Density by construction. - static double KStarMass(); - double SampledQ2Density(double mD, double mK, double ml, double q2, bool apply_va) const; - double SampledQ2Normalization(double mD, double mK, double ml, bool apply_va) const; - // Per-component normalization cache (not serialized; keyed by mass set). + // Per-component q^2-normalization cache (not serialized; keyed by mass set). + // Closure helpers live in charm_decay:: (CharmDecayKinematics.h). mutable std::map norm_cache; public: CharmMesonDecay(); CharmMesonDecay(siren::dataclasses::Particle::ParticleType primary); virtual bool equal(Decay const & other) const override; - static double particleMass(siren::dataclasses::ParticleType particle); - // Analytic angle-average of the accepted V-A weight (q^2 density factor). - // Public so the unit tests can cross-check it against a numeric quadrature; - // it is a pure function of the decay masses and m23. - double VAWeightAngleAverage(double mD, double mK, double ml, double m23) const; double TotalDecayWidth(dataclasses::InteractionRecord const &) const override; double TotalDecayWidth(siren::dataclasses::Particle::ParticleType primary) const override; double TotalDecayWidthForFinalState(dataclasses::InteractionRecord const &) const override; diff --git a/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h b/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h index 77324ebb9..a36a07029 100644 --- a/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h +++ b/projects/interactions/public/SIREN/interactions/CharmMesonDecay3Body.h @@ -2,16 +2,11 @@ #ifndef SIREN_CharmMesonDecay3Body_H #define SIREN_CharmMesonDecay3Body_H -// CharmMesonDecay3Body -- Pythia-style 3-body phase-space decay for D mesons -// -// Sister class to CharmMesonDecay (the legacy 2-body-cascade implementation). -// Both inherit from Decay and share the same decay-width machinery -// (DifferentialDecayWidth, TotalDecayWidthForFinalState, FinalStateProbability, -// and the signature catalog). The only behavioural difference is in -// SampleFinalState, where this class generates the final-state kinematics by -// sampling 3-body phase space (following Pythia's ParticleDecays::threeBody) -// with V-A matrix element reweighting. It also mixes D -> K l nu with -// D -> K*(892) l nu channels per event, in line with PDG branching ratios. +// CharmMesonDecay3Body -- Pythia-style 3-body phase-space decay for D0/D+. +// Sampler draws final-state kinematics from 3-body phase space +// (Pythia ParticleDecays::threeBody) with V-A matrix-element reweighting and +// per-event K / K*(892) kinematic mixing (PDG branching ratios). Independent of +// CharmMesonDecay; shares only the inline charm_decay:: closure kinematics. #include #include @@ -46,22 +41,13 @@ class CharmMesonDecay3Body : public Decay { friend cereal::access; private: const std::set primary_types = {siren::dataclasses::Particle::ParticleType::D0, siren::dataclasses::Particle::ParticleType::DPlus}; - // Shared closure helpers: SampleFinalState's density and FinalStateProbability - // both build on these, so Sample == Density by construction. - static double KStarMass(); - double SampledQ2Density(double mD, double mK, double ml, double q2, bool apply_va) const; - double SampledQ2Normalization(double mD, double mK, double ml, bool apply_va) const; - // Per-component normalization cache (not serialized; keyed by mass set). + // Per-component q^2-normalization cache (not serialized; keyed by mass set). + // Closure helpers live in charm_decay:: (CharmDecayKinematics.h). mutable std::map norm_cache; public: CharmMesonDecay3Body(); CharmMesonDecay3Body(siren::dataclasses::Particle::ParticleType primary); virtual bool equal(Decay const & other) const override; - static double particleMass(siren::dataclasses::ParticleType particle); - // Analytic angle-average of the accepted V-A weight (q^2 density factor). - // Public so the unit tests can cross-check it against a numeric quadrature; - // pure function of the decay masses and m23. - double VAWeightAngleAverage(double mD, double mK, double ml, double m23) const; double TotalDecayWidth(dataclasses::InteractionRecord const &) const override; double TotalDecayWidth(siren::dataclasses::Particle::ParticleType primary) const override; double TotalDecayWidthForFinalState(dataclasses::InteractionRecord const &) const override; diff --git a/projects/interactions/public/SIREN/interactions/DMesonELoss.h b/projects/interactions/public/SIREN/interactions/DMesonELoss.h index 483aaefbb..229386bf9 100644 --- a/projects/interactions/public/SIREN/interactions/DMesonELoss.h +++ b/projects/interactions/public/SIREN/interactions/DMesonELoss.h @@ -41,13 +41,10 @@ friend cereal::access; std::set target_types_ = {siren::dataclasses::Particle::ParticleType::PPlus}; // Truncation bounds for the inelasticity z, shared by SampleFinalState - // (rejection) and DifferentialCrossSection/FinalStateProbability (normalization). - // The ACTUAL upper limit is the energy-dependent kinematic cut z <= 1 - mD/E - // (final_energy >= mD); both the sampler and the density apply it, and the - // Gaussian is normalized over [z_min_, min(z_max_, 1 - mD/E)] so the supports - // match exactly at every energy (closure). z_max_ = 1 lets kinematics set the - // top; z_min_ is a small floor keeping z > 0 (no energy gain) and away from the - // z -> 0 null-recoil degeneracy. + // (rejection) and the density (normalization). The actual upper limit is the + // energy-dependent kinematic cut z <= 1 - mD/E; the Gaussian is normalized over + // [z_min_, min(z_max_, 1 - mD/E)] so supports match at every energy (closure). + // z_min_ floors z > 0 (no energy gain, away from the null-recoil degeneracy). static constexpr double z_min_ = 0.001; static constexpr double z_max_ = 1.0; diff --git a/tests/python/test_pythia_charm_validation.py b/tests/python/test_pythia_charm_validation.py index 2526f67c4..0744524d5 100644 --- a/tests/python/test_pythia_charm_validation.py +++ b/tests/python/test_pythia_charm_validation.py @@ -368,27 +368,3 @@ def test_end_to_end_rate_closure(): upper = sigma_cm2 * n_Ar * L_max_cm assert 0.0 < sum_w < upper, f"sum_w {sum_w:.3e} not in (0, sigma*n*L_max={upper:.3e})" assert sum_w > 0.02 * upper, f"sum_w {sum_w:.3e} implausibly small vs thin-target {upper:.3e}" - - -# --------------------------------------------------------------------------- -# Test 6: per-event SampleFinalState cost (production throughput guard) -# --------------------------------------------------------------------------- -@pytest.mark.skipif(not PYTHIA_DATA, reason="set PYTHIA8DATA to run the Pythia-sampling tests") -def test_sample_final_state_perf_budget(): - """PythiaDISCrossSection re-initializes Pythia per event (variable-energy mode - is unsupported for WeakBosonExchange). Guard the per-event cost so a major - regression that would make PeV production infeasible is caught. Generous - ceiling; the measured value is reported. - """ - import time - xs = _make_xs(with_differential=False) - M = 10 - t0 = time.time() - ev = _sample_siren(xs, 1.0e4, n=M) - elapsed = time.time() - t0 - assert len(ev) >= M // 2, "too few events sampled to time" - per_event = elapsed / len(ev) - print(f"\nSampleFinalState mean per-event cost: {per_event:.3f} s") - assert per_event < 3.0, ( - f"SampleFinalState is {per_event:.2f} s/event (> 3 s ceiling) -- a " - "Pythia re-init regression would make large-scale production infeasible.") From 79715cbda27657126ef98b793040e1ad66ed399f Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sun, 28 Jun 2026 20:16:15 -0700 Subject: [PATCH 92/93] charm-DIS tests: trim verbose comments Collapse the long file-header docstrings and per-test paragraphs that restated closure logic documented in the production headers, and remove inline narration. Comment-only. --- .../DetectorModelDegenerateDirection_TEST.cxx | 51 +++----- .../private/test/CharmSerialization_TEST.cxx | 27 ++-- .../private/test/CharmDISClosure_TEST.cxx | 55 +++------ .../private/test/CharmDecayTestHelpers.h | 5 +- .../test/CharmMesonDecay3Body_TEST.cxx | 62 +++------- .../private/test/CharmMesonDecay_TEST.cxx | 88 +++++-------- .../private/test/DMesonELoss_TEST.cxx | 90 ++++---------- .../test/InteractionSerialization_TEST.cxx | 17 ++- .../test/PythiaDISCharmClosure_TEST.cxx | 84 +++++-------- .../test/QuarkDISDensityContract_TEST.cxx | 51 ++------ projects/math/private/test/Vector3D_TEST.cxx | 19 ++- tests/python/test_pythia_charm_validation.py | 116 +++++++----------- tests/python/test_quarkdis_slow_rescaling.py | 106 +++++----------- 13 files changed, 243 insertions(+), 528 deletions(-) diff --git a/projects/detector/private/test/DetectorModelDegenerateDirection_TEST.cxx b/projects/detector/private/test/DetectorModelDegenerateDirection_TEST.cxx index 16803d911..907a7101b 100644 --- a/projects/detector/private/test/DetectorModelDegenerateDirection_TEST.cxx +++ b/projects/detector/private/test/DetectorModelDegenerateDirection_TEST.cxx @@ -1,27 +1,12 @@ // Degenerate trajectory-direction behavior for DetectorModel depth queries. -// -// DetectorModel computes a trajectory direction as (p1 - p0) in cartesian -// coordinates. When p0 and p1 are at Earth-scale coordinates and only -// micrometers apart, the subtraction suffers catastrophic cancellation, so the -// resulting direction is unreliable; for coincident points it is the zero -// vector. The required behavior in these cases: -// - Vector3D::normalize() must not divide by zero (no NaN/Inf). -// - The interaction-depth sub-threshold path returns the well-defined decay -// term (distance / total_decay_length) rather than normalizing an -// unreliable direction and tripping the unit-direction assert. -// - All depth queries must return finite, non-negative values and must not -// abort. -// -// These tests construct the degenerate cases directly. They use a default -// (infinite-vacuum) DetectorModel, so they are fully self-contained and require -// no external data files. +// DetectorModel computes direction as (p1 - p0); at Earth-scale coordinates a +// sub-micrometer separation cancels catastrophically (coincident points give the +// zero vector). All depth queries must stay finite, non-negative, and not abort. // // NOTE: the unit-direction assert (std::abs(1.0 - std::abs(dot)) < 1e-6) only -// fires in an assertions-enabled build (Debug / RelWithDebInfo); in a fully -// optimized NDEBUG build it is compiled out. The finite/non-NaN return-value -// assertions below exercise and lock in the required numeric behavior in either -// build. +// fires in an assertions-enabled build (Debug / RelWithDebInfo), not under NDEBUG; +// the finite/non-NaN return-value asserts below lock in behavior in either build. #include #include @@ -53,10 +38,8 @@ bool IsFinite(double x) { } // namespace -// p0 and p1 are distinct (so the p0 == p1 early-return does NOT fire) but their -// separation is below distance_threshold. GetInteractionDepthInCGS with non-empty -// targets must short-circuit to distance/total_decay_length rather than -// normalizing the cancellation-corrupted direction and tripping the unit assert. +// Distinct but sub-threshold points with non-empty targets: must short-circuit +// to distance/total_decay_length instead of normalizing the corrupted direction. TEST(DetectorModelDegenerateDirection, InteractionDepthSubThresholdWithTargets) { DetectorModel A; // infinite vacuum sphere, no files @@ -66,8 +49,7 @@ TEST(DetectorModelDegenerateDirection, InteractionDepthSubThresholdWithTargets) Vector3D p1; p1.SetCartesianCoordinates(kEarthScaleX + kSubThresholdOffset, 0.0, 0.0); - // Sanity: the early p0 == p1 guard must NOT catch this (points differ), - // and the separation is genuinely below threshold. + // p0 != p1 (early guard must not fire), yet separation is below threshold. ASSERT_TRUE(p0 != p1); double distance = (p1 - p0).magnitude(); ASSERT_GT(distance, 0.0); @@ -85,8 +67,7 @@ TEST(DetectorModelDegenerateDirection, InteractionDepthSubThresholdWithTargets) EXPECT_TRUE(IsFinite(depth)) << "InteractionDepth must be finite, got " << depth; - // At sub-threshold distance the result is the decay-only limit, matching - // the targets.empty() branch (continuity across the threshold). + // Sub-threshold result is the decay-only limit (continuous with targets.empty()). double expected = distance / total_decay_length; EXPECT_NEAR(expected, depth, std::abs(expected) * 1e-9 + 1e-30); EXPECT_GE(depth, 0.0); @@ -104,7 +85,7 @@ TEST(DetectorModelDegenerateDirection, InteractionDepthSubThresholdDecayOnly) p1.SetCartesianCoordinates(kEarthScaleX + kSubThresholdOffset, 0.0, 0.0); double distance = (p1 - p0).magnitude(); - std::vector targets; // empty -> decay only + std::vector targets; // empty: decay-only branch std::vector total_cross_sections; double total_decay_length = 2.5e2; @@ -118,9 +99,8 @@ TEST(DetectorModelDegenerateDirection, InteractionDepthSubThresholdDecayOnly) std::abs(distance / total_decay_length) * 1e-9 + 1e-30); } -// GetColumnDepthInCGS on a sub-threshold but distinct segment normalizes a tiny -// (magnitude ~1e-7) direction. The difference is small-but-nonzero, so the call -// must complete without aborting and return a finite, non-negative value. +// Column depth on a sub-threshold but distinct segment normalizes a tiny (~1e-7) +// direction; must complete without aborting and return finite, non-negative. TEST(DetectorModelDegenerateDirection, ColumnDepthSubThresholdIsFinite) { DetectorModel A; @@ -162,10 +142,9 @@ TEST(DetectorModelDegenerateDirection, ParticleColumnDepthSubThresholdIsFinite) } } -// Exact-coincidence case (p0 == p1) at Earth-scale coordinates. Here (p1 - p0) -// is the zero vector. The early p0 == p1 guard returns the degenerate limit, and -// the Vector3D::normalize() zero-length guard protects any path that still -// normalizes it. All depth queries must return 0, finite, and not produce NaN. +// Exact coincidence (p0 == p1) at Earth-scale: (p1 - p0) is the zero vector. The +// early guard plus Vector3D::normalize()'s zero-length guard must keep every depth +// query at 0, finite, no NaN. TEST(DetectorModelDegenerateDirection, CoincidentPointsReturnZeroFinite) { DetectorModel A; diff --git a/projects/injection/private/test/CharmSerialization_TEST.cxx b/projects/injection/private/test/CharmSerialization_TEST.cxx index 025fe775a..6bd55d6b5 100644 --- a/projects/injection/private/test/CharmSerialization_TEST.cxx +++ b/projects/injection/private/test/CharmSerialization_TEST.cxx @@ -1,15 +1,9 @@ // Serialization round-trip tests for the charm-DIS decay classes and the Weighter. -// -// Two invariants are enforced: -// 1. CharmMesonDecay / CharmMesonDecay3Body must consume their entire -// serialized body on load. Because both are default-constructible, cereal -// uses a member load() rather than load_and_construct (it does not invoke -// load_and_construct for a default-constructible type). The trailing -// sentinel after each decay reads back correctly only if the body is fully -// consumed, so it directly detects a load that skips the body. -// 2. A Weighter holding a charm decay process must survive SaveWeighter -// followed by reconstruction through the filename constructor (LoadWeighter) -// and re-serialize byte-identically. +// 1. CharmMesonDecay / CharmMesonDecay3Body must fully consume their body on +// load (both default-constructible, so cereal uses member load(), not +// load_and_construct). A trailing sentinel detects a load that skips the body. +// 2. A Weighter holding a charm decay process must survive SaveWeighter + +// filename-ctor reconstruction (LoadWeighter) and re-serialize byte-identically. #include #include @@ -42,8 +36,7 @@ using siren::detector::DetectorModel; namespace { // Round-trip a polymorphic Decay through a binary archive followed by a sentinel -// int. The sentinel reads back correctly only if the decay body was fully -// consumed on load -- i.e. it detects a load that leaves bytes in the stream. +// int (which reads back correctly only if the body was fully consumed on load). std::shared_ptr roundtrip_decay_with_sentinel(std::shared_ptr orig, bool & sentinel_ok) { const int kSentinel = 0x5A5A5A; @@ -73,8 +66,6 @@ std::string read_file(std::string const & path) { } } // namespace -// --- Polymorphic Decay round-trips (load path consumes the full body) -------- - TEST(CharmSerialization, CharmMesonDecayPolymorphicRoundTrip) { std::shared_ptr orig = std::make_shared(ParticleType::D0); bool sentinel_ok = false; @@ -101,8 +92,7 @@ TEST(CharmSerialization, CharmMesonDecay3BodyPolymorphicRoundTrip) { EXPECT_TRUE(sentinel_ok) << "decay body was not fully consumed on load"; } -// --- Weighter SaveWeighter/LoadWeighter with a charm decay process ----------- - +// Weighter SaveWeighter/LoadWeighter with a charm decay process. TEST(CharmSerialization, WeighterWithCharmDecaySaveLoad) { auto detector_model = std::make_shared(); std::shared_ptr decay = std::make_shared(ParticleType::D0); @@ -124,8 +114,7 @@ TEST(CharmSerialization, WeighterWithCharmDecaySaveLoad) { ASSERT_NO_THROW(w.SaveWeighter(base)); // writes .siren_weighter - // The filename constructor calls LoadWeighter to reconstruct the weighter - // from the serialized file. + // Filename ctor calls LoadWeighter to reconstruct from the file. std::unique_ptr w2; ASSERT_NO_THROW(w2.reset(new Weighter(no_injectors, base))); ASSERT_NE(w2, nullptr); diff --git a/projects/interactions/private/test/CharmDISClosure_TEST.cxx b/projects/interactions/private/test/CharmDISClosure_TEST.cxx index 53eb316f4..404c1107b 100644 --- a/projects/interactions/private/test/CharmDISClosure_TEST.cxx +++ b/projects/interactions/private/test/CharmDISClosure_TEST.cxx @@ -1,27 +1,10 @@ -// Regression test for the charm-DIS interaction-depth closure invariant. -// -// Background: a charm-DIS cross section registers three D-type final-state -// signatures (D0, D+, Ds) that share a single inclusive charm total cross -// section sigma(E). The interaction depth that sets the vertex distribution is -// computed two ways that MUST agree: -// -// generation side : CrossSection::TotalCrossSectionAllFinalStates(record) -// (called by the ~10 vertex/position distributions) -// physical side : sum over GetPossibleSignaturesFromParents of -// TotalCrossSection(signature) (Weighter.tcc) -// -// The base-class default TotalCrossSectionAllFinalStates SUMS TotalCrossSection -// over the signatures, so the two sides agree only if no subclass overrides -// TotalCrossSectionAllFinalStates to short-circuit the sum. Additionally, the -// inclusive sigma must be PARTITIONED across the D species by fragmentation -// fraction, otherwise the sum triple-counts charm production. -// -// This test does not need Pythia8: it uses a mock that reproduces the two -// behaviors of PythiaDISCrossSection (TotalCrossSection independent of meson -// type; three registered D-type signatures) and exercises the real base-class -// CrossSection::TotalCrossSectionAllFinalStates. It guards the invariant the -// PythiaDISCrossSection fix relies on. The companion gtest -// PythiaDISCharmClosure_TEST exercises the real class on a Pythia-enabled build. +// Regression test for the charm-DIS interaction-depth closure invariant: the +// generation side (TotalCrossSectionAllFinalStates) and physical side (sum of +// per-signature TotalCrossSection, Weighter.tcc) must agree, and the inclusive +// sigma must be partitioned across the three D species by fragmentation fraction +// (else the sum triple-counts). Pythia-free: a mock reproduces PythiaDIS's two +// relevant behaviors and exercises the real base-class TotalCrossSectionAllFinalStates. +// Companion PythiaDISCharmClosure_TEST exercises the real class. #include #include @@ -50,9 +33,8 @@ class MockCharmXS : public CrossSection { bool override_afs; MockCharmXS(bool ff, bool ovr) : apply_ff(ff), override_afs(ovr) {} - // D0:D+/-:Ds = 0.60:0.23:0.15 renormalized to sum to 1.0 (Lambda_c not - // modeled, its fraction redistributed). Mirrors - // QuarkDISFromSpline::FragmentationFraction. + // D0:D+/-:Ds = 0.60:0.23:0.15 each /0.98 to sum to 1.0 (unmodeled Lambda_c + // redistributed). Mirrors QuarkDISFromSpline::FragmentationFraction. static double FragmentationFraction(ParticleType d) { if(d==ParticleType::D0 || d==ParticleType::D0Bar) return 0.6 / 0.98; if(d==ParticleType::DPlus || d==ParticleType::DMinus) return 0.23 / 0.98; @@ -130,8 +112,8 @@ double gen_path(MockCharmXS const & xs, ParticleType primary, ParticleType targe } // namespace -// Reproduces the f1751c6b bug: the override makes the generation side report 1x -// while the physical side reports 3x -> closure broken by a factor of 3. +// f1751c6b bug: override makes the generation side report 1x while the physical +// side reports 3x -> closure broken by a factor of 3. TEST(CharmDISClosure, OverrideBreaksClosureByFactorThree) { MockCharmXS xs(/*ff=*/false, /*override=*/true); for(double E : {10.0, 100.0, 1000.0}) { @@ -143,8 +125,8 @@ TEST(CharmDISClosure, OverrideBreaksClosureByFactorThree) { } } -// Reproduces 4b7baf4a (override removed, no FF): the two sides agree (closure -// restored) but both equal 3x the inclusive sigma -> charm production overcounted. +// 4b7baf4a (override removed, no FF): sides agree (closure restored) but both +// equal 3x the inclusive sigma -> charm production overcounted. TEST(CharmDISClosure, OverrideRemovedClosesButOvercountsThreeX) { MockCharmXS xs(/*ff=*/false, /*override=*/false); for(double E : {10.0, 100.0, 1000.0}) { @@ -155,14 +137,10 @@ TEST(CharmDISClosure, OverrideRemovedClosesButOvercountsThreeX) { } } -// With the fragmentation fraction applied per signature in TotalCrossSection -// (and the FFs renormalized to sum to 1.0), the generation-side and physical- -// side inclusive charm cross sections must agree AND both equal the full -// inclusive sigma -- partitioned across the three D species, not triple-counted. +// With FF applied per signature (FFs renormalized to sum to 1.0), both sides +// agree AND equal the full inclusive sigma -- partitioned, not triple-counted. TEST(CharmDISClosure, FragmentationFractionRestoresPhysicalNormalization) { MockCharmXS xs(/*ff=*/true, /*override=*/false); - // FF sum = (0.6 + 0.23 + 0.15) / 0.98 == 1.0 (renormalized), so the - // partitioned total recovers the full inclusive sigma exactly. for(double E : {10.0, 100.0, 1000.0}) { double gen = gen_path(xs, ParticleType::NuMu, ParticleType::PPlus, E); double phys = phys_path(xs, ParticleType::NuMu, ParticleType::PPlus, E); @@ -172,8 +150,7 @@ TEST(CharmDISClosure, FragmentationFractionRestoresPhysicalNormalization) { } } -// The three implemented fragmentation fractions must partition the inclusive -// charm cross section exactly: they sum to 1.0 (Lambda_c fraction redistributed). +// The three implemented fragmentation fractions sum to 1.0. TEST(CharmDISClosure, FragmentationFractionsSumToOne) { double sum = MockCharmXS::FragmentationFraction(ParticleType::D0) + MockCharmXS::FragmentationFraction(ParticleType::DPlus) diff --git a/projects/interactions/private/test/CharmDecayTestHelpers.h b/projects/interactions/private/test/CharmDecayTestHelpers.h index 225beda44..d78b4f786 100644 --- a/projects/interactions/private/test/CharmDecayTestHelpers.h +++ b/projects/interactions/private/test/CharmDecayTestHelpers.h @@ -2,9 +2,8 @@ #ifndef SIREN_CharmDecayTestHelpers_H #define SIREN_CharmDecayTestHelpers_H -// Shared numeric oracle + record helpers for the CharmMesonDecay closure tests. -// Both decay test files include this so the V-A angle-average cross-check and -// the q^2 reconstruction stay in one place. +// Shared q^2 reconstruction + V-A angle-average numeric oracle for the +// CharmMesonDecay closure tests (included by both decay test files). #include diff --git a/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx b/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx index c625334ef..c74600fc6 100644 --- a/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx +++ b/projects/interactions/private/test/CharmMesonDecay3Body_TEST.cxx @@ -1,29 +1,9 @@ /** - * Unit test for CharmMesonDecay3Body -- Pythia-style 3-body phase-space decay - * of charm mesons with V-A matrix-element reweighting and K / K*(892) kinematic - * mixing. - * - * D (m0) -> K (m1) + lepton (m2) + neutrino (m3) - * - * All three daughters are constructed in the D rest frame and boosted to the - * lab with the SAME boost, so 4-momentum is conserved exactly (up to float). - * A per-event coin flip draws the hadron mass as either the pseudoscalar K mass - * or the K*(892) mass (= Constants::KPrimePlusMass via KStarMass()). - * - * Tests: - * 1. ThreeBodyEnergyMomentumConservation -- sum of the three secondary - * 4-momenta equals the primary 4-momentum component-wise. - * 2. DaughterMassShells -- lepton/neutrino on shell, hadron mass equals either - * the pseudoscalar K or the K*(892) mass, and m23 = sqrt((p_l+p_nu)^2) lies - * in the allowed window [ml, mD - mK]. - * 3. KStarMixingFraction -- the empirical K vs K* split matches the PDG-derived - * fracK within a few binomial sigma (D0 and D+). - * 4. TotalDecayWidthAndBranchingSums -- per-signature widths are positive, the - * three branching ratios partition unity, TotalDecayWidth equals the sum - * over signatures, and bad signatures throw. - * 5. FinalStateProbabilityClosure -- FinalStateProbability is non-negative, - * equals 1 for the fully hadronic mode, and the sampled q^2 distribution - * closes against FinalStateProbability bin-by-bin within MC error. + * Unit tests for CharmMesonDecay3Body -- Pythia-style 3-body phase-space decay + * D -> K + lepton + neutrino with V-A reweighting and K/K*(892) mass mixing. + * Covers 4-momentum conservation, daughter mass shells / m23 window, the K vs + * K* mixing fraction, decay-width & branching bookkeeping, and the + * SampleFinalState <-> FinalStateProbability q^2 closure. */ #include #include @@ -85,8 +65,7 @@ TEST(CharmMesonDecay3Body, ThreeBodyEnergyMomentumConservation) { double E_D = 100.0; auto rng = std::make_shared(); - // both semileptonic modes (e and mu) - std::vector> cases = { + std::vector> cases = { // e and mu modes {sigs[0], Constants::electronMass}, {sigs[1], Constants::muonMass}, }; @@ -137,9 +116,8 @@ TEST(CharmMesonDecay3Body, DaughterMassShells) { return std::sqrt(std::max(0.0, m2)); }; - // lepton on shell, neutrino massless. - EXPECT_NEAR(mass(pl), ml, 1e-5); - EXPECT_NEAR(mass(pnu), 0.0, 1e-5); + EXPECT_NEAR(mass(pl), ml, 1e-5); // lepton on shell + EXPECT_NEAR(mass(pnu), 0.0, 1e-5); // neutrino massless // hadron mass is one of the two mixture components. double mK = mass(pK); @@ -276,10 +254,9 @@ TEST(CharmMesonDecay3Body, FinalStateProbabilityClosure) { EXPECT_NEAR(decay.FinalStateProbability(rec), 1.0, 1e-12); } - // (b) Sample many semileptonic events, histogram q^2 separately for the K - // and K* sub-populations, and compare the empirical density per bin to - // fractions[comp] * FinalStateProbability evaluated at a record built at - // that q^2 with the matching hadron mass. + // (b) Histogram sampled q^2 per K/K* sub-population and compare each bin's + // empirical density to FinalStateProbability (which folds in the mixture + // weight) at a record rebuilt at that q^2 with the matching hadron mass. const int NB = 16; double q2lo = 0.0; double q2hi = (mD - mK) * (mD - mK); // widest support (K mass) @@ -305,9 +282,7 @@ TEST(CharmMesonDecay3Body, FinalStateProbabilityClosure) { else countKstar[b]++; } - // Build a record at a target q^2 in the D rest frame with hadron mass comp_mK - // and evaluate FinalStateProbability there (it already includes the K/K* - // mixture weight fractions[comp]). + // FinalStateProbability at a target q^2 in the D rest frame, hadron mass comp_mK. auto fsp_at_q2 = [&](double comp_mK, double q2) -> double { double EK = (mD * mD + comp_mK * comp_mK - q2) / (2 * mD); double pk_sq = EK * EK - comp_mK * comp_mK; @@ -316,7 +291,7 @@ TEST(CharmMesonDecay3Body, FinalStateProbabilityClosure) { InteractionRecord r; r.signature = sig; r.primary_mass = mD; - r.primary_momentum = {mD, 0, 0, 0}; // D at rest (q^2 frame-independent) + r.primary_momentum = {mD, 0, 0, 0}; // D at rest; q^2 frame-independent r.secondary_momenta = {{EK, PK, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; r.secondary_masses = {comp_mK, ml, 0.0}; return decay.FinalStateProbability(r); @@ -340,10 +315,8 @@ TEST(CharmMesonDecay3Body, FinalStateProbabilityClosure) { } // --- Analytic angle-average matches a numeric quadrature oracle ------------ -// -// charm_decay::VAWeightAngleAverage (used by the weighting code) must reproduce -// a numeric quadrature of the identical clamped V-A weight -// (numericVAWeightAngleAverage, in CharmDecayTestHelpers.h). +// charm_decay::VAWeightAngleAverage (used by the weighting code) must match the +// numeric quadrature numericVAWeightAngleAverage (CharmDecayTestHelpers.h). TEST(CharmMesonDecay3Body, VAWeightAngleAverageMatchesNumericReference) { struct Case { double mD; double mK; double ml; }; std::vector cases = { @@ -368,9 +341,8 @@ TEST(CharmMesonDecay3Body, VAWeightAngleAverageMatchesNumericReference) { } } -// CharmMesonDecay3Body implements only D0 and D+. Unsupported species (e.g. Ds) -// and empty-signature records must fail loudly rather than silently mis-decay or -// index out of bounds. (CharmMesonDecay covers D0/D+/Ds and anti-flavors.) +// Only D0 and D+ are implemented here; unsupported species (e.g. Ds) and +// empty-signature records must throw rather than mis-decay or index out of bounds. TEST(CharmMesonDecay3Body, UnsupportedSpeciesAndEmptySignatureThrow) { EXPECT_THROW({ CharmMesonDecay3Body d(ParticleType::DsPlus); }, std::runtime_error); CharmMesonDecay3Body dp(ParticleType::DPlus); diff --git a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx index 8f47eaa21..b28aea26b 100644 --- a/projects/interactions/private/test/CharmMesonDecay_TEST.cxx +++ b/projects/interactions/private/test/CharmMesonDecay_TEST.cxx @@ -1,20 +1,7 @@ /** - * Unit test for CharmMesonDecay CDF / Interpolator1D behavior and the - * SampleFinalState <-> FinalStateProbability closure invariant. - * - * Tests: - * 1. Interpolator1D with a known linear inverse CDF (sanity check) - * 2. Interpolator1D with the actual D meson decay CDF table - * 3. SampleFinalState q^2 distribution closes with FinalStateProbability - * 4. TotalDecayWidthForFinalState throws on unsupported signatures - * - * Build (from SIREN/build): - * make -j4 (if registered in CMakeLists.txt) - * Or standalone: - * g++ -std=c++17 -I../projects/utilities/public -I../projects/interactions/public \ - * -I../projects/dataclasses/public -I../vendor/cereal/include \ - * -I../vendor/rk/include -I../vendor/photospline/include \ - * -L. -lSIREN -o CharmMesonDecay_TEST CharmMesonDecay_TEST.cxx -lgtest -lgtest_main + * Unit tests for CharmMesonDecay: Interpolator1D inverse-CDF behavior, the + * SampleFinalState <-> FinalStateProbability q^2 closure, decay-length physics, + * and loud failure on unsupported signatures. */ #include #include @@ -48,11 +35,9 @@ using namespace siren::dataclasses; using siren::interactions::charm_decay_test::reconstruct_q2; using siren::interactions::charm_decay_test::numericVAWeightAngleAverage; -// --- Test 1: Interpolator1D with known linear CDF ------------------------ +// --- Test 1: Interpolator1D with known linear inverse CDF F(x)=x/1.4 ------ TEST(Interpolator1D, LinearInverseCDF) { - // CDF: F(x) = x / 1.4, so inverse: x = 1.4 * u - // Table: x = CDF values, f = Q2 values TableData1D table; int N = 102; for (int i = 0; i < N; ++i) { @@ -64,7 +49,6 @@ TEST(Interpolator1D, LinearInverseCDF) { Interpolator1D interp(table); - // Check known values double tol = 0.01; EXPECT_NEAR(interp(0.0), 0.0, tol); EXPECT_NEAR(interp(0.25), 0.35, tol); @@ -72,7 +56,7 @@ TEST(Interpolator1D, LinearInverseCDF) { EXPECT_NEAR(interp(0.75), 1.05, tol); EXPECT_NEAR(interp(1.0), 1.4, tol); - // Sample mean should be 0.7 + // Sample mean must be 0.7 (uniform on [0,1.4]). double sum = 0; int Nsamp = 10000; SIREN_random rng; @@ -96,7 +80,6 @@ TEST(Interpolator1D, DMesonDecayCDF) { double ms = 2.00697; double GF = Constants::FermiConstant; - // DifferentialDecayWidth (fixed version) auto dGamma = [&](double Q2) -> double { double Q2tilde = Q2 / (ms * ms); double ff2 = std::pow(F0CKM / ((1 - Q2tilde) * (1 - alpha * Q2tilde)), 2); @@ -107,7 +90,8 @@ TEST(Interpolator1D, DMesonDecayCDF) { return std::pow(GF, 2) / (24 * std::pow(M_PI, 3)) * ff2 * std::pow(PK, 3); }; - // Normalize (same as SIREN: Romberg integration over [0, 1.4]) + // Normalization and CDF-table construction mirror SIREN exactly: Romberg + // over [0, 1.4], then 100 trapezoid nodes from 0.01 to ~1.39. std::function pdf_func = dGamma; double norm = rombergIntegrate(pdf_func, 0.0, 1.4); @@ -115,7 +99,6 @@ TEST(Interpolator1D, DMesonDecayCDF) { return dGamma(Q2) / norm; }; - // Build CDF table (same as SIREN: 100 nodes from 0.01 to ~1.39) double Q2_min = 0.0; double Q2_max = 1.4; std::vector Q2spline; @@ -149,7 +132,7 @@ TEST(Interpolator1D, DMesonDecayCDF) { cdf_vector.push_back(1.0); pdf_vector.push_back(0); - // Build Interpolator1D (inverse CDF: x=CDF, f=Q2) + // Inverse CDF: x=CDF, f=Q2. TableData1D inverse_cdf_data; inverse_cdf_data.x = cdf_vector; inverse_cdf_data.f = cdf_Q2_nodes; @@ -165,7 +148,6 @@ TEST(Interpolator1D, DMesonDecayCDF) { for (int i = 0; i < Nsamp; ++i) { double u = rng.Uniform(0, 1); sum_q2 += inverse_cdf(u); - // Linear interpolation for comparison double q2_lin = 0; for (size_t j = 0; j < cdf_vector.size() - 1; ++j) { if (cdf_vector[j] <= u && u <= cdf_vector[j + 1]) { @@ -186,8 +168,8 @@ TEST(Interpolator1D, DMesonDecayCDF) { // --- Test 3: SampleFinalState q^2 closes with FinalStateProbability ------- namespace { -// Build a record at a given q^2 in the D rest frame with hadron mass mK so that -// FinalStateProbability can be evaluated on a single mixture component. +// Record at a given q^2 in the D rest frame with hadron mass mK (one mixture +// component), for evaluating FinalStateProbability. InteractionRecord make_record_at_q2(const InteractionSignature & sig, double mD, double mK, double ml, double q2) { double EK = (mD * mD + mK * mK - q2) / (2 * mD); @@ -204,7 +186,8 @@ InteractionRecord make_record_at_q2(const InteractionSignature & sig, } // namespace TEST(CharmMesonDecay, SampledQ2Distribution) { - // D0 -> K- e+ nu_e with kinematic K/K*(892) mixing. + // D0 -> K- e+ nu_e with kinematic K/K*(892) mixing; guards SampleFinalState + // against FinalStateProbability via normalization, mean, and bin-by-bin q^2. CharmMesonDecay decay(ParticleType::D0); auto sigs = decay.GetPossibleSignaturesFromParent(ParticleType::D0); auto sig = sigs[0]; @@ -261,8 +244,8 @@ TEST(CharmMesonDecay, SampledQ2Distribution) { double var_sampled = sum_q2_sq / Nsamp - mean_sampled * mean_sampled; double stderr_mean = std::sqrt(var_sampled / Nsamp); - // --- (1) Normalization: integral of FinalStateProbability over q^2, - // summed over the K and K* mixture, must equal 1. --- + // (1) FinalStateProbability integrated over q^2, summed over the K/K* + // mixture, must normalize to 1. auto fsp_integral = [&](double comp_mK) -> double { std::function integrand = [&](double q2) -> double { InteractionRecord r = make_record_at_q2(sig, mD, comp_mK, ml, q2); @@ -277,8 +260,8 @@ TEST(CharmMesonDecay, SampledQ2Distribution) { double total_norm = normK + normKstar; EXPECT_NEAR(total_norm, 1.0, 1e-2); - // --- (2) Closure of the mean: mean_density (in-test quadrature of - // FinalStateProbability) must match mean_sampled within MC error. --- + // (2) Mean closure: quadrature of q^2 * FinalStateProbability matches the + // sampled mean within MC error. auto fsp_q2_integral = [&](double comp_mK) -> double { std::function integrand = [&](double q2) -> double { InteractionRecord r = make_record_at_q2(sig, mD, comp_mK, ml, q2); @@ -291,13 +274,11 @@ TEST(CharmMesonDecay, SampledQ2Distribution) { double mean_density = (fsp_q2_integral(mK) + fsp_q2_integral(mKstar)) / total_norm; EXPECT_NEAR(mean_sampled, mean_density, 4.0 * stderr_mean); - // --- (3) Bin-by-bin closure per sub-population. For each filled bin the - // empirical density (count/(N*bw), with the sub-population folded - // in via N = total) must match FinalStateProbability at the bin - // center within ~3 sigma Poisson error. --- + // (3) Bin-by-bin closure per sub-population: empirical density count/(N*bw) + // (N = total, folding in the sub-population fraction) matches + // FinalStateProbability at the bin center within Poisson error. for (int b = 0; b < NB; ++b) { double q2c = q2lo + (b + 0.5) * bw; - // K sub-population if (countK[b] > 30 && q2c < (mD - mK) * (mD - mK)) { double emp = (double)countK[b] / (Nsamp * bw); double sigma = std::sqrt((double)countK[b]) / (Nsamp * bw); @@ -305,7 +286,6 @@ TEST(CharmMesonDecay, SampledQ2Distribution) { double pred = decay.FinalStateProbability(r); EXPECT_NEAR(emp, pred, 4.0 * sigma + 0.02 * pred); } - // K* sub-population if (countKstar[b] > 30 && q2c < (mD - mKstar) * (mD - mKstar)) { double emp = (double)countKstar[b] / (Nsamp * bw); double sigma = std::sqrt((double)countKstar[b]) / (Nsamp * bw); @@ -321,7 +301,7 @@ TEST(CharmMesonDecay, SampledQ2Distribution) { TEST(CharmMesonDecay, UnsupportedSignaturesThrow) { CharmMesonDecay decay(ParticleType::D0); - // Positive control: a valid D0 signature has positive width. + // Positive control. auto sigs = decay.GetPossibleSignaturesFromParent(ParticleType::D0); InteractionRecord good; good.signature = sigs[0]; @@ -345,11 +325,8 @@ TEST(CharmMesonDecay, UnsupportedSignaturesThrow) { } // --- Test 5: analytic angle-average matches a numeric quadrature oracle ----- -// -// The weighting code (FinalStateProbability via SampledQ2Density) uses the -// closed-form charm_decay::VAWeightAngleAverage. It must reproduce a numeric -// quadrature of the identical clamped V-A weight (numericVAWeightAngleAverage, -// in CharmDecayTestHelpers.h), evaluated with the same rk::P4 boosts. +// charm_decay::VAWeightAngleAverage (used by the weighting code) must match the +// numeric quadrature numericVAWeightAngleAverage (CharmDecayTestHelpers.h). TEST(CharmMesonDecay, VAWeightAngleAverageMatchesNumericReference) { struct Case { double mD; double mK; double ml; }; std::vector cases = { @@ -378,20 +355,14 @@ TEST(CharmMesonDecay, VAWeightAngleAverageMatchesNumericReference) { } // --- Test 6: lab decay length L = beta*gamma*c*tau (cascade separation) ------ -// -// The multi-cascade search reconstructs the separation L between the production -// cascade and the charm-decay cascade; the hard regime is below ~10 m. L is set -// by the D species proper lifetime and the lab boost, so it must be physically -// correct across the analysis energy band (TeV-PeV). Decay::TotalDecayLength -// returns beta*gamma*(1/Gamma)*hbarc, and because the modeled branching ratios +// Decay::TotalDecayLength = beta*gamma*(1/Gamma)*hbarc; since the modeled BRs // sum to 1 the width recovers the PDG lifetime exactly. namespace { constexpr double tau_D0_s = 410.1e-15; // proper lifetimes hardcoded in constexpr double tau_Dp_s = 1040.0e-15; // CharmMesonDecay (seconds). constexpr double tau_Ds_s = 504.0e-15; constexpr double c_m_per_s = 2.99792458e8; // speed of light [m/s] -// Note: Decay::TotalDecayLength returns METERS (SIREN base length unit; hbarc -// carries the cm->m conversion), consistent with the Taupede reco frame. +// TotalDecayLength returns METERS (hbarc carries the cm->m conversion). InteractionRecord make_boosted_D(ParticleType d, double mD, double E_D) { double p = std::sqrt(E_D * E_D - mD * mD); @@ -399,7 +370,7 @@ InteractionRecord make_boosted_D(ParticleType d, double mD, double E_D) { rec.signature.primary_type = d; rec.signature.target_type = ParticleType::Decay; rec.primary_mass = mD; - rec.primary_momentum = {E_D, 0.0, 0.0, p}; // boosted along +z + rec.primary_momentum = {E_D, 0.0, 0.0, p}; return rec; } } // namespace @@ -416,14 +387,14 @@ TEST(CharmMesonDecay, LabDecayLengthIsBetaGammaCTau) { double prev_L = -1.0, prev_E = -1.0; for (double E_D : {1.0e4, 1.0e5, 1.0e6}) { // 10 TeV, 100 TeV, 1 PeV InteractionRecord rec = make_boosted_D(cs.d, cs.mD, E_D); - double L_m = decay.TotalDecayLength(rec); // SIREN [m] + double L_m = decay.TotalDecayLength(rec); double p = std::sqrt(E_D * E_D - cs.mD * cs.mD); double betagamma = p / cs.mD; - double expected_m = betagamma * c_m_per_s * cs.tau; // physics truth [m] + double expected_m = betagamma * c_m_per_s * cs.tau; // 0.5% absorbs the ~0.01% rounding of SIREN's hbar/hbarc constants. EXPECT_NEAR(L_m, expected_m, 5e-3 * expected_m) << "species=" << static_cast(cs.d) << " E_D=" << E_D; - // beta*gamma is linear in E for E >> m: L(10x E) ~ 10x L. + // beta*gamma linear in E for E >> m: L(10x E) ~ 10x L. if (prev_L > 0.0) EXPECT_NEAR(L_m / prev_L, E_D / prev_E, 1e-3 * (E_D / prev_E)); prev_L = L_m; prev_E = E_D; @@ -446,8 +417,7 @@ TEST(CharmMesonDecay, LabDecayLengthSpeciesOrdering) { } TEST(CharmMesonDecay, FinalStateProbabilityThrowsOnEmptySignature) { - // finalize() does not copy the signature; FinalStateProbability must reject - // an empty-signature record loudly instead of indexing out of bounds (UB). + // Empty signature must throw, not index out of bounds (finalize() drops it). CharmMesonDecay decay(ParticleType::D0); InteractionRecord rec; // default: empty signature, no secondaries EXPECT_THROW(decay.FinalStateProbability(rec), std::runtime_error); diff --git a/projects/interactions/private/test/DMesonELoss_TEST.cxx b/projects/interactions/private/test/DMesonELoss_TEST.cxx index ee2d3c29e..01c9b88c7 100644 --- a/projects/interactions/private/test/DMesonELoss_TEST.cxx +++ b/projects/interactions/private/test/DMesonELoss_TEST.cxx @@ -1,30 +1,6 @@ -/** - * Unit test for DMesonELoss (charm-meson energy-loss "cross section"). - * - * DMesonELoss models a D meson losing a fraction z of its energy to a hadronic - * vertex. The signature is D -> {same D, Hadrons} on a PPlus target. The - * inelasticity z is drawn from a Gaussian (z0=0.56, sigma=0.2) truncated to - * [z_min_, z_max_] = [0.001, 0.999] via rejection; the same truncated Gaussian - * is the density returned by DifferentialCrossSection / FinalStateProbability, - * so Sample == Density (closure). - * - * Tests: - * 1. EnergyMonotonicallyDecreases -- outgoing D energy is always in - * (D mass, primary energy); the D never gains energy. - * 2. FourMomentumAndMassInvariants -- outgoing D is on its mass shell, - * energy is conserved (E0 == E_D + E_H), and the two outgoing 3-momenta - * are collinear with the incoming direction (momentum is NOT conserved by - * design -- the hadron is massless and collinear). - * 3. TotalCrossSectionPositiveAndThrows -- TotalCrossSection is positive and - * increasing in E over the validity range, and throws on an unsupported - * primary. - * 4. SubThresholdThrows -- SampleFinalState throws siren::utilities:: - * InjectionFailure when the primary energy is at or below the D mass. - * 5. ZDensityClosure -- the empirical sampled-z distribution matches - * FinalStateProbability/DifferentialCrossSection (the truncated Gaussian) - * bin-by-bin within Poisson error (closure of the realized sampler against - * the advertised density). - */ +// Unit tests for DMesonELoss: D -> {same D, Hadrons} energy-loss model. The +// inelasticity z is a truncated Gaussian that is also the advertised density, +// so Sample == Density (closure). See DMesonELoss.h for the physics. #include #include #include @@ -66,8 +42,7 @@ InteractionRecord make_d0_record(const InteractionSignature & sig, double E_D) { } // namespace -// --- Test 1: the D meson always loses energy -------------------------------- - +// The D meson always loses energy: E_out in (mD, E_D). TEST(DMesonELoss, EnergyMonotonicallyDecreases) { DMesonELoss xs; auto sigs = xs.GetPossibleSignaturesFromParents(ParticleType::D0, ParticleType::PPlus); @@ -87,17 +62,14 @@ TEST(DMesonELoss, EnergyMonotonicallyDecreases) { cdr.Finalize(rec); double E_out = rec.secondary_momenta[0][0]; - // The D meson never gains energy (z >= z_min_ > 0). - EXPECT_LT(E_out, E_D); - // It stays on (or above) its mass shell -- no z>z_max_ leakage. - EXPECT_GE(E_out, mD); + EXPECT_LT(E_out, E_D); // never gains energy (z >= z_min_ > 0) + EXPECT_GE(E_out, mD); // stays on/above mass shell (no z>z_max_ leakage) EXPECT_GT(E_out, 0.0); } } } -// --- Test 2: on-shell, energy conservation, collinearity -------------------- - +// Outgoing D on-shell, energy conserved, both products collinear with +z. TEST(DMesonELoss, FourMomentumAndMassInvariants) { DMesonELoss xs; auto sig = xs.GetPossibleSignaturesFromParents(ParticleType::D0, ParticleType::PPlus)[0]; @@ -115,36 +87,32 @@ TEST(DMesonELoss, FourMomentumAndMassInvariants) { const auto & pdm = rec.secondary_momenta[0]; // outgoing D const auto & ph = rec.secondary_momenta[1]; // hadron - // (a) outgoing D is on its mass shell. + // outgoing D on its mass shell. double m2 = pdm[0]*pdm[0] - pdm[1]*pdm[1] - pdm[2]*pdm[2] - pdm[3]*pdm[3]; EXPECT_NEAR(std::sqrt(std::max(0.0, m2)), mD, 1e-6); - // (b) energy is conserved: E0 == E_D_out + E_H. + // energy conserved; hadron carries the loss and is ~massless. EXPECT_NEAR(pdm[0] + ph[0], E_D, 1e-6); - // hadron carries the lost energy and is (nearly) massless. EXPECT_GT(ph[0], 0.0); double mh2 = ph[0]*ph[0] - ph[1]*ph[1] - ph[2]*ph[2] - ph[3]*ph[3]; EXPECT_NEAR(mh2, 0.0, 1e-6); - // (c) collinearity: both outgoing 3-momenta lie along +z (the incoming - // direction). Momentum is NOT conserved by design (massless hadron), - // so we assert direction, not p-balance. + // Both products collinear with +z. Momentum is NOT conserved by design + // (massless hadron), so assert direction, not p-balance. EXPECT_NEAR(pdm[1], 0.0, 1e-9); EXPECT_NEAR(pdm[2], 0.0, 1e-9); EXPECT_GT(pdm[3], 0.0); EXPECT_NEAR(ph[1], 0.0, 1e-9); EXPECT_NEAR(ph[2], 0.0, 1e-9); EXPECT_GT(ph[3], 0.0); - // direction of p_D matches the incoming +z direction. - EXPECT_GT(p_D, 0.0); + EXPECT_GT(p_D, 0.0); // incoming +z direction } } -// --- Test 3: total cross section positivity / monotonicity / throws --------- - +// TotalCrossSection positive, increasing in E, and throws on bad primary. TEST(DMesonELoss, TotalCrossSectionPositiveAndThrows) { DMesonELoss xs; - // Positive and increasing with energy over the (>1 PeV) validity range. + // Positive and increasing over the (>1 PeV) validity range. double last = -1.0; for (double E : {1e6, 1e7, 1e8, 1e9}) { double s = xs.TotalCrossSection(ParticleType::D0, E); @@ -152,19 +120,17 @@ TEST(DMesonELoss, TotalCrossSectionPositiveAndThrows) { if (last >= 0.0) EXPECT_GT(s, last); last = s; } - // Unsupported primary throws. EXPECT_THROW(xs.TotalCrossSection(ParticleType::PiPlus, 1e7), std::runtime_error); } -// --- Test 4: sub-threshold primary fails recoverably ------------------------ - +// Sub-threshold primary fails recoverably with InjectionFailure. TEST(DMesonELoss, SubThresholdThrows) { DMesonELoss xs; auto sig = xs.GetPossibleSignaturesFromParents(ParticleType::D0, ParticleType::PPlus)[0]; auto rng = std::make_shared(); double mD = Constants::D0Mass; - // Primary energy exactly at the D mass (D at rest): no valid final state. + // Energy exactly at the D mass (D at rest): no valid final state. { InteractionRecord rec; rec.signature = sig; @@ -176,7 +142,7 @@ TEST(DMesonELoss, SubThresholdThrows) { CrossSectionDistributionRecord cdr(rec); EXPECT_THROW(xs.SampleFinalState(cdr, rng), siren::utilities::InjectionFailure); } - // Primary energy below the D mass. + // Energy below the D mass. { double E_D = 0.5 * mD; InteractionRecord rec; @@ -191,8 +157,7 @@ TEST(DMesonELoss, SubThresholdThrows) { } } -// --- Test 5: sampled-z density matches FinalStateProbability ---------------- - +// Empirical sampled-z distribution matches FinalStateProbability bin-by-bin. TEST(DMesonELoss, ZDensityClosureAcrossEnergies) { DMesonELoss xs; auto sig = xs.GetPossibleSignaturesFromParents(ParticleType::D0, ParticleType::PPlus)[0]; @@ -200,11 +165,10 @@ TEST(DMesonELoss, ZDensityClosureAcrossEnergies) { const double mD = Constants::D0Mass; const double zlo = 0.001; // == z_min_ - // Span low to high primary energy. At low E the kinematic cut z <= 1 - mD/E - // truncates the Gaussian well below its mean (z0 = 0.56) -- exactly where a - // fixed-interval density (normalizing over [z_min_, z_max_] regardless of E) - // mis-normalizes and breaks closure. The density now applies the same - // energy-dependent cut, so Sample == Density at every energy. + // At low E the kinematic cut z <= 1 - mD/E truncates the Gaussian below its + // mean (z0=0.56) -- where a fixed-interval density would mis-normalize and + // break closure. The density applies the same E-dependent cut, so Sample == + // Density at every energy. Span low to high E to exercise that. for (double E_D : {5.0, 10.0, 50.0, 200.0, 1000.0, 3000.0}) { const double z_kin = 1.0 - mD / E_D; // kinematic upper limit const double zhi = std::min(1.0, z_kin); // == min(z_max_, z_kin) @@ -221,7 +185,7 @@ TEST(DMesonELoss, ZDensityClosureAcrossEnergies) { xs.SampleFinalState(cdr, rng); cdr.Finalize(rec); double z = 1.0 - rec.secondary_momenta[0][0] / E_D; - // The sampler must respect the same support the density advertises. + // sampler must respect the support the density advertises. EXPECT_GE(z, zlo - 1e-9) << "E_D=" << E_D; EXPECT_LE(z, zhi + 1e-9) << "E_D=" << E_D; int b = (int)((z - zlo) / bw); @@ -243,7 +207,7 @@ TEST(DMesonELoss, ZDensityClosureAcrossEnergies) { return xs.FinalStateProbability(r); }; - // (a) bin-by-bin closure within ~4 Poisson sigma. + // bin-by-bin closure within ~4 Poisson sigma. for (int b = 0; b < NB; ++b) { double zc = zlo + (b + 0.5) * bw; double pred = fsp_at_z(zc); @@ -254,7 +218,7 @@ TEST(DMesonELoss, ZDensityClosureAcrossEnergies) { EXPECT_NEAR(emp, pred, 4.0 * sg + 0.02 * pred) << "E_D=" << E_D << " z=" << zc; } - // (b) the density integrates to 1 over the realized support [zlo, zhi]. + // density integrates to 1 over the realized support [zlo, zhi]. int M = 4000; double integral = 0.0, dz = (zhi - zlo) / M; for (int i = 0; i <= M; ++i) { @@ -264,8 +228,8 @@ TEST(DMesonELoss, ZDensityClosureAcrossEnergies) { } EXPECT_NEAR(integral, 1.0, 2e-2) << "E_D=" << E_D; - // (c) the density vanishes ABOVE the kinematic cut -- the fixed-interval - // bug was nonzero density where the sampler produces nothing. + // density vanishes ABOVE the kinematic cut (the fixed-interval bug left + // nonzero density where the sampler produces nothing). double z_above = zhi + 0.4 * (1.0 - zhi); if (z_above < 1.0 - 1e-9) { EXPECT_EQ(fsp_at_z(z_above), 0.0) << "E_D=" << E_D << " z_above=" << z_above; diff --git a/projects/interactions/private/test/InteractionSerialization_TEST.cxx b/projects/interactions/private/test/InteractionSerialization_TEST.cxx index 4abeaf31b..eda822ee1 100644 --- a/projects/interactions/private/test/InteractionSerialization_TEST.cxx +++ b/projects/interactions/private/test/InteractionSerialization_TEST.cxx @@ -1,11 +1,9 @@ // Serialization round-trip tests for non-default-constructible interaction -// classes that rely on load_and_construct. cereal only recognizes a STATIC -// load_and_construct (or a non-member specialization); a non-static one is -// silently ignored, leaving the serialized body unread and corrupting whatever -// follows it in the archive. The trailing sentinel after each object reads back -// correctly only if the body is fully consumed, so it directly detects a load -// that skips the body. (DarkNewsDecay is intentionally excluded: it is abstract -// / python-backed and cannot be constructed by cereal here.) +// classes relying on load_and_construct. cereal only recognizes a STATIC +// load_and_construct; a non-static one is silently ignored, leaving the body +// unread and corrupting the rest of the archive. A trailing sentinel after each +// object reads back correctly only if the body was fully consumed, so it detects +// a load that skips the body. (DarkNewsDecay excluded: abstract/python-backed.) #include #include @@ -32,9 +30,8 @@ using namespace siren::interactions; using siren::dataclasses::ParticleType; namespace { -// Round-trip a polymorphic base pointer through a binary archive, followed by a -// sentinel int. The sentinel reads back correctly only if the object's body was -// fully consumed on load. +// Round-trip a polymorphic base pointer through a binary archive followed by a +// sentinel int (which reads back correctly only if the body was fully consumed). template std::shared_ptr roundtrip_with_sentinel(std::shared_ptr orig, bool & sentinel_ok) { const int kSentinel = 0x5A5A5A; diff --git a/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx b/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx index eb2218545..4f3d744dc 100644 --- a/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx +++ b/projects/interactions/private/test/PythiaDISCharmClosure_TEST.cxx @@ -1,25 +1,9 @@ -// Regression test for the PythiaDISCrossSection charm-DIS interaction-depth -// closure / normalization bug. -// -// Requires a Pythia8-enabled build (SIREN_WITH_PYTHIA8=ON) AND total/differential -// charm-DIS spline files supplied via environment variables: -// -// SIREN_PYTHIA_TEST_DSDXDY -> differential (dsdxdy) FITS spline -// SIREN_PYTHIA_TEST_SIGMA -> total (sigma) FITS spline -// -// If the variables are unset the test SKIPs (it only needs the spline tables, -// not LHAPDF/Pythia at runtime: the total-cross-section path never calls Pythia). -// -// What it guards: -// * Per-signature TotalCrossSection must be PARTITIONED by fragmentation -// fraction (D0:0.6, D+:0.23, Ds:0.15), not equal to the full inclusive sigma. -// * sum over signatures of TotalCrossSection == sigma_inclusive * 0.98 (NOT 3x). -// * TotalCrossSectionAllFinalStates (generation side) == that sum (physical -// side) -> closure holds. -// -// Before the fix (no fragmentation fraction in TotalCrossSection) every signature -// returns the full inclusive sigma, the sum is 3x too large, and the partition -// assertions FAIL. After the fix they PASS. +// Regression tests for the PythiaDISCrossSection charm-DIS interaction-depth +// closure bug. Guards that per-signature TotalCrossSection is partitioned by +// fragmentation fraction (sum == inclusive sigma, not 3x) and that the +// generation side (TotalCrossSectionAllFinalStates) equals the physical sum. +// See PythiaDISCrossSection.h. Requires SIREN_WITH_PYTHIA8 plus spline files in +// env vars SIREN_PYTHIA_TEST_DSDXDY / SIREN_PYTHIA_TEST_SIGMA; SKIPs if unset. #include #include @@ -43,9 +27,8 @@ using namespace siren::dataclasses; namespace { double expected_ff(ParticleType d) { - // Renormalized to sum to 1.0 over the implemented D species (the unmodeled - // Lambda_c fraction is folded in by dividing each raw value by 0.98). Mirrors - // PythiaDISCrossSection::FragmentationFraction. + // Raw fractions / 0.98 to fold in the unmodeled Lambda_c and renormalize to + // sum to 1.0. Mirrors PythiaDISCrossSection::FragmentationFraction. if(d==ParticleType::D0 || d==ParticleType::D0Bar) return 0.6 / 0.98; if(d==ParticleType::DPlus || d==ParticleType::DMinus) return 0.23 / 0.98; if(d==ParticleType::DsPlus || d==ParticleType::DsMinus) return 0.15 / 0.98; @@ -89,7 +72,6 @@ TEST(PythiaDISCharmClosure, FragmentationPartitionAndClosure) { sum += v; per_sig.push_back(v); - // identify the D meson in this signature and check FF partition ParticleType d = ParticleType::unknown; for(auto t : sig.secondary_types) if(isD(t)) { d = t; break; } ASSERT_NE(d, ParticleType::unknown); @@ -97,14 +79,14 @@ TEST(PythiaDISCharmClosure, FragmentationPartitionAndClosure) { << "signature D-type cross section is not fragmentation-partitioned at E=" << E; } - // The three values must differ (0.6 vs 0.23 vs 0.15), i.e. NOT all == sigma_incl. + // The three values must differ (0.6 vs 0.23 vs 0.15), not all == sigma_incl. EXPECT_GT(std::abs(per_sig[0] - per_sig[1]), sigma_incl * 1e-6); - // Sum is the inclusive sigma (partitioned), not 3x. + // Sum is the partitioned inclusive sigma, not 3x. EXPECT_NEAR(sum, sigma_incl * ff_sum, sigma_incl * 1e-9); EXPECT_LT(sum, sigma_incl * 1.5); // would be 3x without the fix - // Generation side (TotalCrossSectionAllFinalStates) == physical side (sum). + // Generation side == physical side (sum). InteractionRecord rec; rec.signature.primary_type = ParticleType::NuMu; rec.signature.target_type = ParticleType::PPlus; @@ -114,10 +96,9 @@ TEST(PythiaDISCharmClosure, FragmentationPartitionAndClosure) { } } -// The differential spline is optional. With only a total spline (empty -// differential filename), FinalStateProbability must return a constant 1.0 -- -// the Pythia final-state density is intractable but cancels in the unbiased -// weight, so only the total cross section matters. Needs only the total spline. +// With only a total spline (empty differential filename), FinalStateProbability +// returns a constant 1.0: the intractable Pythia final-state density cancels in +// the unbiased weight, so only the total cross section matters. TEST(PythiaDISCharmClosure, ConstantFinalStateProbabilityWithoutDifferential) { const char* tt = std::getenv("SIREN_PYTHIA_TEST_SIGMA"); if(!tt) { @@ -131,11 +112,9 @@ TEST(PythiaDISCharmClosure, ConstantFinalStateProbabilityWithoutDifferential) { /*minimum_Q2=*/1.0, primaries, targets, /*pythia_data_path=*/"", /*pdf_set=*/"LHAPDF6:CT18NLO", /*units=*/"cm"); - // Total still works. EXPECT_GT(xs.TotalCrossSection(ParticleType::NuMu, 100.0), 0.0); - // FinalStateProbability is exactly 1.0 for any well-formed record, since the - // no-differential branch returns before touching kinematics. + // FSP is exactly 1.0: the no-differential branch returns before kinematics. auto sig = xs.GetPossibleSignaturesFromParents(ParticleType::NuMu, ParticleType::PPlus)[0]; InteractionRecord rec; rec.signature = sig; @@ -146,9 +125,8 @@ TEST(PythiaDISCharmClosure, ConstantFinalStateProbabilityWithoutDifferential) { EXPECT_EQ(xs.FinalStateProbability(rec), 1.0); } -// PythiaDISCrossSection forces charm only in charged current. NC charm is not -// forced by Z exchange, so it must be rejected at construction with an -// actionable error rather than failing later inside SampleFinalState. +// Charm is forced only in CC; NC charm must be rejected at construction (not +// later inside SampleFinalState) with an actionable error. TEST(PythiaDISCharmClosure, NeutralCurrentRejectedAtConstruction) { const char* tt = std::getenv("SIREN_PYTHIA_TEST_SIGMA"); if(!tt) GTEST_SKIP() << "Set SIREN_PYTHIA_TEST_SIGMA to run."; @@ -167,11 +145,9 @@ TEST(PythiaDISCharmClosure, NeutralCurrentRejectedAtConstruction) { }); } -// Species/fragmentation tripwire. The three modeled D fractions are renormalized -// (each /0.98) so the partitioned signatures recover the inclusive charm cross -// section; the unmodeled Lambda_c (~0.02 of the D0/D+/Ds total) is folded in. -// Pin the raw sum (0.98) and Lambda_c -> 0 so adding Lambda_c forces a test -// update rather than silently shifting the species mix. +// Species tripwire: pin the renormalized fractions, the raw sum (0.98), and +// Lambda_c -> 0 so modeling Lambda_c forces a test update rather than silently +// shifting the species mix. TEST(PythiaDISCharmClosure, FragmentationFractionSpeciesTripwire) { const char* tt = std::getenv("SIREN_PYTHIA_TEST_SIGMA"); if(!tt) GTEST_SKIP() << "Set SIREN_PYTHIA_TEST_SIGMA to run."; @@ -183,21 +159,19 @@ TEST(PythiaDISCharmClosure, FragmentationFractionSpeciesTripwire) { EXPECT_NEAR(xs.FragmentationFraction(ParticleType::D0), 0.6 / 0.98, 1e-12); EXPECT_NEAR(xs.FragmentationFraction(ParticleType::DPlus), 0.23 / 0.98, 1e-12); EXPECT_NEAR(xs.FragmentationFraction(ParticleType::DsPlus), 0.15 / 0.98, 1e-12); - // Renormalized fractions over the modeled species recover the full inclusive sigma. + // Renormalized fractions recover the full inclusive sigma. double sum = xs.FragmentationFraction(ParticleType::D0) + xs.FragmentationFraction(ParticleType::DPlus) + xs.FragmentationFraction(ParticleType::DsPlus); EXPECT_NEAR(sum, 1.0, 1e-12); - // Raw fractions sum to 0.98; the 0.02 gap is the folded unmodeled-baryon fraction. + // Raw fractions sum to 0.98; the 0.02 gap is the folded unmodeled baryon. EXPECT_NEAR(0.6 + 0.23 + 0.15, 0.98, 1e-12); - // Lambda_c (PDG 4122) is intentionally unmodeled -> FF 0. Modeling it must update this. + // Lambda_c (PDG 4122) intentionally unmodeled -> FF 0. Modeling it must update this. EXPECT_EQ(xs.FragmentationFraction(static_cast(4122)), 0.0); } -// Closure + fragmentation partition must hold across the ANALYSIS energy band -// (TeV-PeV), not only <=300 GeV. Uses a wide total spline (100 GeV - 1 PeV); a -// spline that silently threw or mis-partitioned at PeV would crash or bias the -// production run at exactly the analysis energies. +// Closure + partition must hold across the analysis band (TeV-PeV), not only +// <=300 GeV. Uses a wide total spline (100 GeV - 1 PeV) via SIREN_PYTHIA_WIDE_SIGMA. TEST(PythiaDISCharmClosure, FragmentationClosureAtAnalysisEnergies) { const char* tt = std::getenv("SIREN_PYTHIA_WIDE_SIGMA"); if(!tt) GTEST_SKIP() << "Set SIREN_PYTHIA_WIDE_SIGMA (wide total spline, 100 GeV-1 PeV) to run."; @@ -238,8 +212,8 @@ TEST(PythiaDISCharmClosure, FragmentationClosureAtAnalysisEnergies) { } } -// Out-of-range differential evaluation must RAISE, not silently return 0 (a -// silent zero on a sampled event biases its weight). Needs a differential spline. +// Out-of-range differential evaluation must RAISE, not return 0 (a silent zero +// on a sampled event biases its weight). TEST(PythiaDISCharmClosure, DifferentialOutOfRangeRaises) { const char* dd = std::getenv("SIREN_PYTHIA_TEST_DSDXDY"); const char* tt = std::getenv("SIREN_PYTHIA_TEST_SIGMA"); @@ -251,9 +225,9 @@ TEST(PythiaDISCharmClosure, DifferentialOutOfRangeRaises) { // In range -> finite positive density (Q2 computed from x,y). EXPECT_GT(xs.DifferentialCrossSection(100.0, 0.1, 0.5, 0.105), 0.0); - // Energy outside the spline extent must raise. + // Energy outside the spline extent raises. EXPECT_THROW(xs.DifferentialCrossSection(1.0e12, 0.1, 0.5, 0.105), std::runtime_error); - // (x, y) outside the spline grid must raise (explicit Q2 bypasses the Q2 cut). + // (x, y) outside the spline grid raises (explicit Q2 bypasses the Q2 cut). EXPECT_THROW(xs.DifferentialCrossSection(100.0, 1.0e-8, 0.5, 0.105, 5.0), std::runtime_error); } #endif // SIREN_HAS_PYTHIA8 diff --git a/projects/interactions/private/test/QuarkDISDensityContract_TEST.cxx b/projects/interactions/private/test/QuarkDISDensityContract_TEST.cxx index b2f014849..818da9f31 100644 --- a/projects/interactions/private/test/QuarkDISDensityContract_TEST.cxx +++ b/projects/interactions/private/test/QuarkDISDensityContract_TEST.cxx @@ -1,28 +1,8 @@ -/** - * Contract test for QuarkDISFromSpline (slow-rescaling charm DIS sampler). - * - * Invariant under test: SampleFinalState samples (xi, y) AND an independently- - * sampled fragmentation z and a uniform azimuth phi that set the D-meson - * momentum, but the advertised density (DensityVariables / FinalStateProbability / - * DifferentialCrossSection) accounts for (xi, y) only. The omitted z/phi factors - * cancel in the weight ratio ONLY in the standard unbiased configuration (the - * same cross-section object supplies both the injection and physical densities - * and no biased phase-space channel is installed on the D kinematics). - * - * These tests do NOT require a spline file: the no-arg ctor only calls - * normalize_pdf() and compute_cdf() for the fragmentation pdf. - * - * Tests: - * 1. ContractPinsTwoDensityVariables -- DensityVariables() must contain exactly - * {"Bjorken xi", "Bjorken y"}. This is an intentional TRIPWIRE: if a future - * change adds the fragmentation-z (or phi) factor to the density, this test - * MUST be updated in lockstep, which forces the closure implications to be - * reconsidered (the z/phi factors then no longer simply cancel). - * 2. FragmentationPdfNormalized -- the fragmentation pdf sample_pdf(z) is a - * properly normalized density over (0.001, 0.999): its integral is 1. This - * documents that the z density EXISTS and is normalized, i.e. the - * alternative (carry z in the density) path is feasible. - */ +// Contract tests for QuarkDISFromSpline (slow-rescaling charm DIS sampler). +// SampleFinalState also draws fragmentation z and azimuth phi, but the density +// covers (xi, y) only; z/phi cancel in the unbiased weight ratio. See +// QuarkDISFromSpline.h. No spline file needed: the no-arg ctor only builds the +// fragmentation pdf/cdf. #include #include #include @@ -33,31 +13,24 @@ using namespace siren::interactions; -// --- Test 1: density-variable contract (tripwire) --------------------------- - TEST(QuarkDISDensityContract, ContractPinsTwoDensityVariables) { - // The no-arg ctor builds only the fragmentation pdf/cdf -- no spline needed. QuarkDISFromSpline xs; std::vector vars = xs.DensityVariables(); - // Tripwire: the density covers exactly (xi, y). The independently-sampled - // fragmentation z and azimuth phi are deliberately NOT in the density. If - // this assertion ever needs to change, the closure argument (z/phi cancel in - // the weight ratio) must be re-derived -- do not simply bump the count. + // Tripwire: density covers exactly (xi, y); z and phi are deliberately NOT + // in it. If this must change, re-derive the closure argument (z/phi cancel + // in the weight ratio) -- do not simply bump the count. ASSERT_EQ(vars.size(), 2u); EXPECT_EQ(vars[0], "Bjorken xi"); EXPECT_EQ(vars[1], "Bjorken y"); } -// --- Test 2: fragmentation pdf is normalized -------------------------------- - +// The fragmentation pdf sample_pdf(z) integrates to 1 over [0.001, 0.999]. TEST(QuarkDISDensityContract, FragmentationPdfNormalized) { QuarkDISFromSpline xs; - // sample_pdf(z) divides the unnormalized fragmentation integrand by - // fragmentation_integral (the integral of that integrand over [0.001,0.999]), - // so it must integrate to 1 over the same interval. Composite-trapezoid on a - // fine grid; the integrand is smooth and bounded away from the z=0,1 poles. + // Composite-trapezoid on a fine grid; integrand is smooth and bounded away + // from the z=0,1 poles. const double zlo = 0.001, zhi = 0.999; const int M = 20000; const double dz = (zhi - zlo) / M; @@ -66,7 +39,7 @@ TEST(QuarkDISDensityContract, FragmentationPdfNormalized) { double z = zlo + i * dz; double w = (i == 0 || i == M) ? 0.5 : 1.0; double f = xs.sample_pdf(z); - EXPECT_GE(f, 0.0); // a pdf is non-negative on its support + EXPECT_GE(f, 0.0); // pdf non-negative on its support integral += w * f * dz; } EXPECT_NEAR(integral, 1.0, 1e-3); diff --git a/projects/math/private/test/Vector3D_TEST.cxx b/projects/math/private/test/Vector3D_TEST.cxx index b3dc31a76..0e8c0b394 100644 --- a/projects/math/private/test/Vector3D_TEST.cxx +++ b/projects/math/private/test/Vector3D_TEST.cxx @@ -218,11 +218,9 @@ TEST(Normalize, Operator) EXPECT_TRUE(B != C); } -// Normalizing the zero vector must not divide by zero: the result must stay -// finite (no NaN/Inf) and the vector is left unchanged at magnitude 0. A unit -// direction cannot be defined for a zero-length vector, so leaving it as-is is -// the well-defined contract callers (e.g. a degenerate p1 - p0 direction in -// DetectorModel) rely on. +// Normalizing the zero vector must not divide by zero: stay finite and leave the +// vector unchanged at magnitude 0 (the contract degenerate DetectorModel p1 - p0 +// directions rely on). TEST(Normalize, ZeroVectorDoesNotProduceNaN) { Vector3D Z; @@ -254,15 +252,12 @@ TEST(Normalize, ZeroVectorNormalizedIsFinite) EXPECT_DOUBLE_EQ(0.0, N.magnitude()); } -// A genuinely tiny but nonzero difference of two Earth-scale coordinates must -// still normalize to a finite, unit-length vector. This is the boundary case -// just above the zero-length guard: the result must be a true unit vector, not -// left unchanged. +// Boundary just above the zero-length guard: a tiny but nonzero difference of two +// Earth-scale coordinates must normalize to a true unit vector, not be left as-is. TEST(Normalize, TinyEarthScaleDifferenceIsFiniteUnit) { - // Two points separated by 1e-7 m built at Earth-scale x ~ 6.371e6 m. - // The subtraction is exact here (1e-7 is representable relative to 6.371e6), - // so the magnitude is nonzero and normalize() must yield a unit vector. + // 1e-7 m is exactly representable relative to 6.371e6 m, so the subtraction is + // exact, the magnitude nonzero, and normalize() must yield a unit vector. Vector3D p0; p0.SetCartesianCoordinates(6.371e6, 0.0, 0.0); Vector3D p1; diff --git a/tests/python/test_pythia_charm_validation.py b/tests/python/test_pythia_charm_validation.py index 0744524d5..aa8b8105d 100644 --- a/tests/python/test_pythia_charm_validation.py +++ b/tests/python/test_pythia_charm_validation.py @@ -1,27 +1,17 @@ """Physics validation for the PythiaDISCrossSection charm-DIS generator. -This is the SIREN-side counterpart of the Pythia-vs-SIREN comparison shown in -the IceCube multi-cascade tau/charm update (Diffuse WG): it guards the absolute -charm-production rate, confirms that SampleFinalState reproduces bare Pythia's -DIS kinematics (Bjorken x/y, Q^2), and that an optional differential spline -covers the realized sampling support (no silent-zero weight bias), across the -analysis energy band (TeV-PeV). - -Everything here needs Pythia8/LHAPDF at runtime and a wide-range total (and, -for the coverage test, differential) charm spline. It is therefore gated behind -environment variables and skips cleanly when they are unset: +Guards the absolute charm-production rate, that SampleFinalState reproduces bare +Pythia's DIS kinematics (Bjorken x/y, Q^2), and that an optional differential +spline covers the realized sampling support (no silent-zero weight bias) across +the TeV-PeV band. Needs Pythia8/LHAPDF at runtime plus charm splines; gated behind +env vars, skips cleanly when unset: SIREN_PYTHIA_WIDE_SIGMA -> total sigma(E) FITS spline, 100 GeV - 1 PeV SIREN_PYTHIA_WIDE_DSDXDY -> differential d2sigma/dx dy FITS spline (optional) LHAPDF_DATA_PATH -> LHAPDF data dir containing the PDF set below -Generate the splines with siren.pythia_charm_splines (see scratch gen script): - - python gen_wide_splines.py - -The SampleFinalState path re-initializes Pythia per event (~1 s/event), so the -SIREN-sampled statistics are deliberately modest; the bare-Pythia reference -(GeneratePythiaCharmSamples, ~ms/event) uses high statistics. +SampleFinalState re-inits Pythia per event (~1 s/event) so SIREN statistics are +modest; the bare-Pythia reference (GeneratePythiaCharmSamples, ~ms/event) is high-stat. """ import math import os @@ -55,9 +45,6 @@ ) -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- def _make_xs(with_differential): import siren.interactions import siren.dataclasses @@ -101,7 +88,7 @@ def _sample_siren(xs, E, n): ir_out.primary_mass = 0.0 cdr.finalize(ir_out) secs = list(ir_out.secondary_momenta) - # secondary order is [charged lepton, Hadrons, D meson] + # secondary order: [charged lepton, Hadrons, D meson] p_lep, p_D = secs[0], secs[2] out.append(dict(x=params["bjorken_x"], y=params["bjorken_y"], E=E, p_lep=p_lep, p_D=p_D, @@ -128,17 +115,13 @@ def _mean(v): return sum(v) / len(v) -# --------------------------------------------------------------------------- -# Test 1: absolute charm-production rate / charm fraction (normalization) -# --------------------------------------------------------------------------- +# Test 1: absolute charm-production rate / charm fraction (normalization). def test_charm_total_cross_section_normalization(): """The inclusive charm-DIS sigma must have the right absolute magnitude. - Pins (a) the charm fraction sigma_charm/sigma_CC at 100 GeV against the - textbook nu-N CC cross section, and (b) sigma_charm/E to the right order of - magnitude across 100 GeV - 1 PeV, with monotonic growth. Guards the rate - that sets S:B and the astro-flux normalization fit -- previously unguarded - (only mocks / relative partitioning existed). + Pins (a) charm fraction sigma_charm/sigma_CC at 100 GeV vs the textbook nu-N CC + cross section, and (b) sigma_charm/E to the right order of magnitude across + 100 GeV - 1 PeV with monotonic growth. """ import siren.dataclasses PT = siren.dataclasses.Particle.ParticleType @@ -165,18 +148,14 @@ def test_charm_total_cross_section_normalization(): assert b > a, "charm cross section must increase with energy" -# --------------------------------------------------------------------------- -# Test 2: SampleFinalState reproduces bare-Pythia DIS kinematics (the slides) -# --------------------------------------------------------------------------- +# Test 2: SampleFinalState reproduces bare-Pythia DIS kinematics. @pytest.mark.skipif(not PYTHIA_DATA, reason="set PYTHIA8DATA to run the Pythia-sampling tests") def test_sampling_matches_bare_pythia_at_100gev(): - """SIREN SampleFinalState must reproduce bare Pythia's Bjorken x/y and Q^2. + """SIREN SampleFinalState must reproduce bare Pythia's Bjorken x/y at 100 GeV. - Reproduces the production-side panels of the Pythia-vs-pythiaSIREN slide at - E_nu = 100 GeV: SampleFinalState extracts/rotates the Pythia final state and - reconstructs (x, y); GeneratePythiaCharmSamples is the same Pythia config - inline. Their distributions must agree -- a frame/extraction bug would show - here as a mismatch. + SampleFinalState extracts/rotates the Pythia final state and reconstructs + (x, y); GeneratePythiaCharmSamples is the same Pythia config inline. A + frame/extraction bug would show here as a distribution mismatch. """ E = 100.0 xs = _make_xs(with_differential=False) @@ -201,30 +180,27 @@ def _stderr(v): f"mean Bjorken-{name}: SIREN={m_si:.4f} vs bare-Pythia={m_py:.4f} " f"(tol {tol:.4f}) -- SampleFinalState does not reproduce Pythia") - # Sampled kinematics must be physical. + # Physical kinematics. assert all(0.0 < e["x"] < 1.0 for e in siren_ev) assert all(0.0 < e["y"] < 1.0 for e in siren_ev) -# --------------------------------------------------------------------------- -# Test 3: D-meson energy fraction + production collimation (morphology) -# --------------------------------------------------------------------------- +# Test 3: D-meson energy fraction + production collimation (morphology). @pytest.mark.skipif(not PYTHIA_DATA, reason="set PYTHIA8DATA to run the Pythia-sampling tests") def test_d_meson_energy_fraction_and_collimation(): - """The D meson carries a sizeable, physical fraction of the event energy and - is produced nearly collinear with the primary lepton at high energy -- the - morphology that sets the two-cascade energy split and separation direction. + """The D meson carries a sizeable, physical energy fraction and is nearly + collinear with the primary lepton at high energy -- the morphology setting the + two-cascade energy split and separation direction. """ E = 1.0e4 # 10 TeV xs = _make_xs(with_differential=False) ev = _sample_siren(xs, E, n=60) assert len(ev) >= 30 zD = [e["zD"] for e in ev] - # Every D carries a physical (0, 1) fraction of the neutrino energy. + # Every D carries a physical (0, 1) energy fraction, mean in a sane range. assert all(0.0 < z < 1.0 for z in zD) - # Mean D energy fraction in a sane charm-DIS range (not ~0, not ~1). assert 0.05 < _mean(zD) < 0.95 - # Opening angle between D and primary lepton is small at 10 TeV (collimated). + # D/lepton opening angle is small at 10 TeV (collimated). def _angle(a, b): import math na = math.sqrt(sum(a[i] ** 2 for i in (1, 2, 3))) @@ -233,22 +209,19 @@ def _angle(a, b): c = max(-1.0, min(1.0, dot / (na * nb))) return math.degrees(math.acos(c)) angles = [_angle(e["p_D"], e["p_lep"]) for e in ev] - # Median opening angle should be modest (DIS at 10 TeV is forward). + # Modest median (DIS at 10 TeV is forward). angles.sort() median = angles[len(angles) // 2] assert median < 30.0, f"median D/lepton opening angle {median:.1f} deg too large" -# --------------------------------------------------------------------------- -# Test 4: optional differential spline covers the sampled support (no silent 0) -# --------------------------------------------------------------------------- +# Test 4: optional differential spline covers the sampled support (no silent 0). @pytest.mark.skipif(not (_have_diff and PYTHIA_DATA), reason="set SIREN_PYTHIA_WIDE_DSDXDY + PYTHIA8DATA to run the coverage test") def test_differential_spline_covers_sampling_support(): - """With a differential spline supplied, DifferentialCrossSection must be - finite-positive on essentially every sampled event, else those events get a - silently-zero physical density and a biased weight. Guards spline (E, x, y) - support vs the realized sampling support at analysis energies. + """With a differential spline, DifferentialCrossSection must be finite-positive + on essentially every sampled event, else those events get a silently-zero + density and a biased weight. Guards spline (E, x, y) support vs realized support. """ xs = _make_xs(with_differential=True) for E in (1.0e3, 1.0e4): # 1 TeV, 10 TeV (within the wide spline x-range) @@ -261,13 +234,13 @@ def test_differential_spline_covers_sampling_support(): try: v = xs.DifferentialCrossSection(e["ir_out"]) except RuntimeError: - out_of_range += 1 # out-of-spline-support correctly RAISES + out_of_range += 1 # out-of-support correctly RAISES continue if math.isfinite(v) and v > 0.0: in_range += 1 else: silent_zero += 1 - # The new contract: out-of-range raises; nothing returns a silent zero. + # Contract: out-of-range raises; nothing returns a silent zero. assert silent_zero == 0, ( f"{silent_zero} sampled events at E={E:.0e} GeV returned a silent-zero " "differential density instead of raising") @@ -277,25 +250,20 @@ def test_differential_spline_covers_sampling_support(): "differential spline (E, x, y) support; widen the spline's logx/logy range.") -# --------------------------------------------------------------------------- -# Test 5: end-to-end inject -> weight through the real Injector / Weighter -# --------------------------------------------------------------------------- +# Test 5: end-to-end inject -> weight through the real Injector / Weighter. @pytest.mark.skipif(not PYTHIA_DATA, reason="set PYTHIA8DATA to run the Pythia-sampling tests") def test_end_to_end_rate_closure(): """Quantitative inject->weight rate closure for charm DIS through the real - _Injector / _Weighter on the CCM detector. - - SIREN folds 1/N into EventWeight and (with injection==physical so the shared - distributions and the cross-section probability cancel, and the injection-side - PointSource position propagator cancels the physical position propagator) - leaves EventWeight_i = InteractionProbability_i / N. Therefore: - - per event, EventWeight must equal GetInteractionProbabilities/N (machine - precision; EventWeight and GetInteractionProbabilities are independent code - paths, so their agreement IS the unbiasedness proof -- a charm - SampleFinalState/FinalStateProbability/TotalCrossSection inconsistency, the - PR#74 closure-break class, would break it), and - - sum(EventWeight) is the unbiased physical-rate estimator == mean(P_int), - which a thin-target estimate sigma * n_Ar * L_eff bounds from above/below. + _Injector / _Weighter on CCM. + + With injection==physical the shared distributions, cross-section probability, + and PointSource position propagator all cancel, leaving EventWeight_i = + InteractionProbability_i / N. Therefore: + - per event, EventWeight == GetInteractionProbabilities/N to machine + precision; they are independent code paths, so agreement IS the + unbiasedness proof -- the PR#74 SampleFinalState/FinalStateProbability/ + TotalCrossSection closure-break class would break it, and + - sum(EventWeight) == mean(P_int), bounded by a thin-target sigma*n_Ar*L_eff. """ import numpy as np from siren import (dataclasses as dc, injection, interactions, distributions, diff --git a/tests/python/test_quarkdis_slow_rescaling.py b/tests/python/test_quarkdis_slow_rescaling.py index 66dcdecb8..ff4c9f84f 100644 --- a/tests/python/test_quarkdis_slow_rescaling.py +++ b/tests/python/test_quarkdis_slow_rescaling.py @@ -1,18 +1,10 @@ """Slow-rescaling (xi, y) sampling tests for QuarkDISFromSpline. -The charm slow-rescaling FITS splines are LHAPDF-derived and not committed to -the repository, so every spline-dependent test is gated behind the -SIREN_CHARM_SPLINE_DIR environment variable. Point it at a directory containing - - dsdxidy_nu-N-cc-charm-CT14nlo_central.fits - sigma_nu-N-cc-charm-CT14nlo_central.fits - -(e.g. the FASRC Maboi_M_Muon_SR spline set) to run the physics asserts. When the -variable is unset, or the FITS files are absent, the whole module skips cleanly. - -Number of events for the differential-positivity test is configurable via -SIREN_CHARM_NEVENTS (default 2000, kept modest so the suite stays fast; set it -to 10000 for a heavier run). +The charm FITS splines are LHAPDF-derived and not committed, so the whole module +is gated behind SIREN_CHARM_SPLINE_DIR (a directory containing +dsdxidy_nu-N-cc-charm-CT14nlo_central.fits and sigma_nu-N-cc-charm-CT14nlo_central.fits); +it skips cleanly when unset or the files are absent. SIREN_CHARM_NEVENTS (default +2000) sizes the differential-positivity test. """ import math import os @@ -21,9 +13,7 @@ siren = pytest.importorskip("siren") -# --------------------------------------------------------------------------- -# Constants (mirror C++ siren::utilities::Constants values exactly) -# --------------------------------------------------------------------------- +# Constants (mirror C++ siren::utilities::Constants values exactly). M_C = 1.27 # Constants::charmMass M_D0 = 1.86484 # Constants::D0Mass M_N = (0.938272 + 0.939565) / 2 # isoscalar nucleon mass @@ -31,19 +21,14 @@ Q2MIN = 1.0 # GeV^2 E_NU = 100.0 # neutrino energy in GeV -# Number of kinematic-bound events (the cheap test) and re-evaluation events -# (the heavier differential-positivity test). -N_BOUNDS = 100 -N_DIFF = int(os.environ.get("SIREN_CHARM_NEVENTS", "2000")) +N_BOUNDS = 100 # cheap kinematic-bounds test +N_DIFF = int(os.environ.get("SIREN_CHARM_NEVENTS", "2000")) # differential test -# Per-event resample budget when the sampler raises a recoverable -# InjectionFailure (surfaces in Python as RuntimeError). The production injector -# framework retries automatically; a direct SampleFinalState caller must do so. +# Per-event resample budget on recoverable InjectionFailure (RuntimeError in +# Python); a direct SampleFinalState caller must retry as the injector does. MAX_RETRIES = 100 -# --------------------------------------------------------------------------- # Spline-file gating: resolve spline paths from SIREN_CHARM_SPLINE_DIR. -# --------------------------------------------------------------------------- _SPLINE_DIR = os.environ.get("SIREN_CHARM_SPLINE_DIR") _DIFF_FILE = ( os.path.join(_SPLINE_DIR, "dsdxidy_nu-N-cc-charm-CT14nlo_central.fits") @@ -73,18 +58,13 @@ ) -# --------------------------------------------------------------------------- -# Expected kinematic bounds (mirror C++ SampleFinalState sampling bounds) -# --------------------------------------------------------------------------- +# Expected kinematic bounds (mirror C++ SampleFinalState sampling bounds). Y_MAX = 1.0 - M_MU / E_NU W2_THR = (M_N + M_D0) ** 2 Y_MIN = (W2_THR - M_N ** 2 + Q2MIN) / (2.0 * M_N * E_NU) XI_MIN = (M_C ** 2 + Q2MIN) / (2.0 * M_N * E_NU * Y_MAX) -# --------------------------------------------------------------------------- -# Fixtures -# --------------------------------------------------------------------------- @pytest.fixture(scope="module") def charm_xs(): """Build the QuarkDISFromSpline cross section once for the module.""" @@ -130,12 +110,10 @@ def _make_neutrino_record(sig): def _sample_with_retries(charm_xs, sig, rng): - """Sample one event, retrying on recoverable InjectionFailure. + """Sample one event, retrying on recoverable InjectionFailure (RuntimeError). - Returns the populated CrossSectionDistributionRecord, or None if the - per-event resample budget was exhausted (an expected, rare NaN-guard - rejection, not a test failure). InjectionFailure derives from - std::runtime_error, so it surfaces in Python as RuntimeError. + Returns the populated record, or None if the resample budget was exhausted + (a rare, expected NaN-guard rejection, not a test failure). """ import siren.dataclasses ir = _make_neutrino_record(sig) @@ -145,18 +123,14 @@ def _sample_with_retries(charm_xs, sig, rng): charm_xs.SampleFinalState(cdr, rng) return cdr except RuntimeError: - # Recoverable per-event sampling failure; resample with fresh - # RNG draws. Hard configuration errors (bad signature, units, or - # spline) would already have fired in the fixtures, not here. + # Recoverable per-event failure; resample. Config errors fire in fixtures. if retry < MAX_RETRIES: continue return None return None -# --------------------------------------------------------------------------- -# Test 1: kinematic bounds on the sampled (xi, y) over 100 events -# --------------------------------------------------------------------------- +# Test 1: kinematic bounds on the sampled (xi, y) over 100 events. def test_quarkdis_kinematic_bounds(charm_xs, signature, rng): """Every sampled event must satisfy the slow-rescaling kinematic bounds.""" assert XI_MIN < 1.0, "degenerate bounds: xi_min >= 1" @@ -178,7 +152,7 @@ def test_quarkdis_kinematic_bounds(charm_xs, signature, rng): y = params["bjorken_y"] x = params["bjorken_x"] - # Derived quantities (mirror C++ slow-rescaling relations). + # Mirror C++ slow-rescaling relations. Q2 = 2.0 * M_N * E_NU * y * xi - M_C ** 2 W2 = M_N ** 2 + 2.0 * M_N * E_NU * y * (1.0 - xi) + M_C ** 2 @@ -202,8 +176,7 @@ def test_quarkdis_kinematic_bounds(charm_xs, signature, rng): f"Q2={Q2:.6g} W2={W2:.6g} | " + "; ".join(event_failures) ) - # The vast majority of slots must produce a sample; a few NaN-guard - # rejections are tolerated but a wholesale failure indicates a real bug. + # Most slots must sample; a few NaN-guard rejections are tolerated. assert sampled > 0, "no events were sampled at all" assert rejected <= N_BOUNDS // 10, ( f"{rejected}/{N_BOUNDS} events were unrecoverable " @@ -215,18 +188,13 @@ def test_quarkdis_kinematic_bounds(charm_xs, signature, rng): ) -# --------------------------------------------------------------------------- -# Test 2: differential cross section is finite-positive on the production path -# --------------------------------------------------------------------------- +# Test 2: differential cross section is finite-positive on the production path. def test_quarkdis_differential_positive(charm_xs, signature, rng): """Re-evaluate the spline on finalized records via the production path. - This drives the weighting density exactly as the Weighter does: - SampleFinalState populates the CrossSectionDistributionRecord, - cdr.finalize(ir_out) materializes the secondary momenta, and - DifferentialCrossSection(ir_out) is evaluated on the finalized record, - which takes the primary-momentum Q2 branch. We assert a high - finite-positive fraction. + Drives the weighting density exactly as the Weighter does: SampleFinalState + -> cdr.finalize(ir_out) -> DifferentialCrossSection on the finalized record + (primary-momentum Q2 branch). Asserts a high finite-positive fraction. """ import siren.dataclasses @@ -252,7 +220,6 @@ def test_quarkdis_differential_positive(charm_xs, signature, rng): ir_out.primary_mass = 0.0 cdr.finalize(ir_out) - # finalize must populate the secondary momenta from the sampled state. assert len(ir_out.secondary_momenta) == n_secondaries, ( f"event {event_idx}: finalize wrote " f"{len(ir_out.secondary_momenta)} secondary momenta, " @@ -288,18 +255,13 @@ def test_quarkdis_differential_positive(charm_xs, signature, rng): ) -# --------------------------------------------------------------------------- -# Test 3: Sample == Density closure on the finalized record -# --------------------------------------------------------------------------- +# Test 3: Sample == Density closure on the finalized record. def test_quarkdis_sample_density_closure(charm_xs, signature, rng): """The two Q2 derivations in DifferentialCrossSection must agree. - DifferentialCrossSection(InteractionRecord) takes the primary-momentum Q2 - branch when the finalized record carries real secondary momenta, and the - stored-(xi, y) fallback branch when momenta are absent. Both must yield the - same differential value for a self-consistently sampled event; FinalState- - Probability (= dxs/txs, the physical density the Weighter uses) must be - finite and non-negative. + Primary-momentum branch (real secondary momenta) vs stored-(xi, y) fallback + branch (momenta absent) must yield the same value for a self-consistent event; + FinalStateProbability (= dxs/txs, the Weighter's density) must be finite >= 0. """ import siren.dataclasses @@ -311,7 +273,7 @@ def test_quarkdis_sample_density_closure(charm_xs, signature, rng): params = dict(cdr.interaction_parameters) - # Finalized record -> primary-momentum Q2 branch. + # Finalized record: primary-momentum Q2 branch. ir_out = siren.dataclasses.InteractionRecord() ir_out.signature = signature ir_out.primary_momentum = [E_NU, 0.0, 0.0, E_NU] @@ -320,11 +282,10 @@ def test_quarkdis_sample_density_closure(charm_xs, signature, rng): dxs_primary = charm_xs.DifferentialCrossSection(ir_out) if not (math.isfinite(dxs_primary) and dxs_primary > 0.0): - # Skip rare edge points where the spline returns 0; closure is only - # meaningful where the differential value is positive on both paths. + # Closure is only meaningful where dxs is positive on both paths. continue - # Same record stripped of secondary momenta -> stored-(xi, y) fallback. + # Same record without secondary momenta: stored-(xi, y) fallback branch. ir_fb = siren.dataclasses.InteractionRecord() ir_fb.signature = signature ir_fb.primary_momentum = [E_NU, 0.0, 0.0, E_NU] @@ -339,8 +300,7 @@ def test_quarkdis_sample_density_closure(charm_xs, signature, rng): } dxs_fallback = charm_xs.DifferentialCrossSection(ir_fb) - # Both branches read the same spline at the same (E, xi, y); they should - # agree to within float roundoff in the Q2 derivation. + # Same spline at the same (E, xi, y): agree to within Q2-derivation roundoff. rel = abs(dxs_primary - dxs_fallback) / max(dxs_primary, dxs_fallback) assert rel < 1e-3, ( f"event {event_idx}: primary-path dxs={dxs_primary:.6g} disagrees " @@ -348,7 +308,7 @@ def test_quarkdis_sample_density_closure(charm_xs, signature, rng): "derivations in DifferentialCrossSection are inconsistent" ) - # Physical density used by the Weighter. + # Weighter's physical density. fsp = charm_xs.FinalStateProbability(ir_out) assert math.isfinite(fsp), f"event {event_idx}: FinalStateProbability not finite: {fsp}" assert fsp >= 0.0, f"event {event_idx}: FinalStateProbability negative: {fsp}" @@ -358,9 +318,7 @@ def test_quarkdis_sample_density_closure(charm_xs, signature, rng): assert checked > 0, "no positive-dxs events were available for closure check" -# --------------------------------------------------------------------------- -# Test 4: absolute charm-DIS normalization (charm fraction vs literature/Pythia) -# --------------------------------------------------------------------------- +# Test 4: absolute charm-DIS normalization (charm fraction vs literature/Pythia). def test_quarkdis_charm_fraction_normalization(): """The inclusive slow-rescaling charm-CC cross section must have the right absolute magnitude: the charm fraction sigma_charm / sigma_CC must land in the From 67c9d486a19a35ebab54d93fffca21dfabbbf96d Mon Sep 17 00:00:00 2001 From: Austin Schneider Date: Sun, 28 Jun 2026 21:21:27 -0700 Subject: [PATCH 93/93] charm-DIS: fix swapped QuarkDIS pybind arg names, includes, example, cmake The QuarkDISFromSpline pybind constructor argument names were swapped relative to the C++ constructors (differential before total), so keyword construction swapped the two splines; name differential_xs_{data,filename} first. Add to the two DIS headers that use std::numeric_limits in a default argument, use the dsdxidy_ slow-rescaling prefix in the DIS_IceCube_charm example, and guard testing.cmake with if(CIBUILDWHEEL). --- cmake/testing.cmake | 2 +- .../private/pybindings/QuarkDISFromSpline.h | 12 ++++++------ .../SIREN/interactions/PythiaDISCrossSection.h | 1 + .../public/SIREN/interactions/QuarkDISFromSpline.h | 1 + resources/examples/example1/DIS_IceCube_charm.py | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/cmake/testing.cmake b/cmake/testing.cmake index 455bf2329..bc6013961 100644 --- a/cmake/testing.cmake +++ b/cmake/testing.cmake @@ -10,7 +10,7 @@ set_target_properties(gtest_main PROPERTIES FOLDER extern) set_target_properties(gmock PROPERTIES FOLDER extern) set_target_properties(gmock_main PROPERTIES FOLDER extern) -if(${CIBUILDWHEEL}) +if(CIBUILDWHEEL) macro(package_add_test TESTNAME) endmacro() else() diff --git a/projects/interactions/private/pybindings/QuarkDISFromSpline.h b/projects/interactions/private/pybindings/QuarkDISFromSpline.h index 1764d4bb9..f65962968 100644 --- a/projects/interactions/private/pybindings/QuarkDISFromSpline.h +++ b/projects/interactions/private/pybindings/QuarkDISFromSpline.h @@ -24,8 +24,8 @@ void register_QuarkDISFromSpline(pybind11::module_ & m) { .def(init<>()) .def(init, std::vector, int, double, double, std::set, std::set, std::string>(), - arg("total_xs_data"), arg("differential_xs_data"), + arg("total_xs_data"), arg("interaction"), arg("target_mass"), arg("minimum_Q2"), @@ -33,8 +33,8 @@ void register_QuarkDISFromSpline(pybind11::module_ & m) { arg("target_types"), arg("units") = std::string("cm")) .def(init, std::vector, int, double, double, std::vector, std::vector, std::string>(), - arg("total_xs_data"), arg("differential_xs_data"), + arg("total_xs_data"), arg("interaction"), arg("target_mass"), arg("minimum_Q2"), @@ -42,8 +42,8 @@ void register_QuarkDISFromSpline(pybind11::module_ & m) { arg("target_types"), arg("units") = std::string("cm")) .def(init, std::set, std::string>(), - arg("total_xs_filename"), arg("differential_xs_filename"), + arg("total_xs_filename"), arg("interaction"), arg("target_mass"), arg("minimum_Q2"), @@ -51,14 +51,14 @@ void register_QuarkDISFromSpline(pybind11::module_ & m) { arg("target_types"), arg("units") = std::string("cm")) .def(init, std::set, std::string>(), - arg("total_xs_filename"), arg("differential_xs_filename"), + arg("total_xs_filename"), arg("primary_types"), arg("target_types"), arg("units") = std::string("cm")) .def(init, std::vector, std::string>(), - arg("total_xs_filename"), arg("differential_xs_filename"), + arg("total_xs_filename"), arg("interaction"), arg("target_mass"), arg("minimum_Q2"), @@ -66,8 +66,8 @@ void register_QuarkDISFromSpline(pybind11::module_ & m) { arg("target_types"), arg("units") = std::string("cm")) .def(init, std::vector, std::string>(), - arg("total_xs_filename"), arg("differential_xs_filename"), + arg("total_xs_filename"), arg("primary_types"), arg("target_types"), arg("units") = std::string("cm")) diff --git a/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h b/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h index 8fc6028fc..45a6ae713 100644 --- a/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h +++ b/projects/interactions/public/SIREN/interactions/PythiaDISCrossSection.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include diff --git a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h index ff17b369f..7758619ca 100644 --- a/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h +++ b/projects/interactions/public/SIREN/interactions/QuarkDISFromSpline.h @@ -9,6 +9,7 @@ #include // for uint32_t #include // for pair #include +#include // for numeric_limits (NaN default arg) #include // for runtime... #include diff --git a/resources/examples/example1/DIS_IceCube_charm.py b/resources/examples/example1/DIS_IceCube_charm.py index df57aef3f..1b28844ae 100644 --- a/resources/examples/example1/DIS_IceCube_charm.py +++ b/resources/examples/example1/DIS_IceCube_charm.py @@ -45,7 +45,7 @@ raise RuntimeError( "SIREN_CHARM_SPLINE_DIR is not set. Set it to the directory containing " "the QuarkDIS charm-target spline files " - "(dsdxdy_nu-N-{cc,nc}-charm-*.fits and sigma_nu-N-{cc,nc}-charm-*.fits) " + "(dsdxidy_nu-N-{cc,nc}-charm-*.fits and sigma_nu-N-{cc,nc}-charm-*.fits) " "before running this example." ) EXPERIMENT = "IceCube" @@ -92,7 +92,7 @@ def make_quark_dis_xs(pdf, target, current_type): int_type = 1 if current_type == "cc" else 2 isoscalar_mass = (0.938272 + 0.939565) / 2 return siren.interactions.QuarkDISFromSpline( - os.path.join(SPLINES_DIR, f"dsdxdy_nu-N-{current_type}-charm-{pdf}.fits"), + os.path.join(SPLINES_DIR, f"dsdxidy_nu-N-{current_type}-charm-{pdf}.fits"), os.path.join(SPLINES_DIR, f"sigma_nu-N-{current_type}-charm-{pdf}.fits"), int(int_type), # interaction type: 1=CC, 2=NC isoscalar_mass,