Add 2D atmospheric flux tables and tabulated trapezoid integration#105
Add 2D atmospheric flux tables and tabulated trapezoid integration#105austinschneider wants to merge 1 commit into
Conversation
Squashed diff for paths: - projects/distributions/private/primary/energy/TabulatedFluxDistribution.cxx - projects/distributions/private/primary/energy_direction/PrimaryEnergyDirectionDistribution.cxx - projects/distributions/private/primary/energy_direction/Tabulated2DFluxDistribution.cxx - projects/distributions/public/SIREN/distributions/primary/energy/TabulatedFluxDistribution.h - projects/distributions/public/SIREN/distributions/primary/energy_direction/PrimaryEnergyDirectionDistribution.h - projects/distributions/public/SIREN/distributions/primary/energy_direction/Tabulated2DFluxDistribution.h - projects/utilities/public/SIREN/utilities/Integration.h - projects/utilities/public/SIREN/utilities/Interpolator.h - projects/distributions/private/pybindings/distributions.cxx - projects/distributions/CMakeLists.txt Source commits on dev/HNL_DIS_clean that contributed: 21451da (Nicholas Kamp): first attempt at external list primary injector 455b073 (Nicholas Kamp): introduce primary bounded and physical vertex distributions to sample vertex in the case that the inital position is already provided from the external distribution 521695f (Nicholas Kamp): fix issues with 2D tabular integral calculation, sampling errors with very short decay lengths, DarkNews errors, and kinematic erros in Dipole portal HNL DIS 73bc119 (Nicholas Kamp): sphere build fixes 82b4fdc (Nicholas Kamp): add ability to trapezoid integrate tabulated 1D fluxes 9098b42 (Nicholas Kamp): fix weighting issue in PrimaryExternalDist 9968ff7 (Nicholas Kamp): Start implementing 2D flux table for atmospheric fluxes 9dae77a (Nicholas Kamp): spherical volume position distribution a79d440 (Nicholas Kamp): getting HNLs to work for SINE and UNDINE a9aebd3 (Nicholas Kamp): fix log integration c7cff6c (Nicholas Kamp): fixing build and runtime errors on 2D flux distribution
cc05a02 to
4a078b6
Compare
b9c0966 to
496977b
Compare
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds support for 2D (energy, zenith) tabulated atmospheric flux distributions and introduces tabulated/trapezoid-style integration options for flux normalization, with corresponding C++ and Python binding updates.
Changes:
- Added
PrimaryEnergyDirectionDistributionandTabulated2DFluxDistribution(C++ + serialization + pybind exposure). - Added 2D Simpson integration and a simple trapezoid integration helper to utilities.
- Extended
TabulatedFluxDistributionto optionally normalize via trapezoid integration and updated Python constructors accordingly.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| projects/utilities/public/SIREN/utilities/Interpolator.h | Exposes IsLogX/IsLogY so callers can adapt sampling/integration to log grids. |
| projects/utilities/public/SIREN/utilities/Integration.h | Adds simpsonIntegrate2D and a simple trapezoidIntegrate helper for tabulated data. |
| projects/distributions/public/SIREN/distributions/primary/energy_direction/Tabulated2DFluxDistribution.h | Declares new 2D flux distribution API + serialization. |
| projects/distributions/public/SIREN/distributions/primary/energy_direction/PrimaryEnergyDirectionDistribution.h | Introduces a new distribution base for coupled energy+direction sampling. |
| projects/distributions/public/SIREN/distributions/primary/energy/TabulatedFluxDistribution.h | Adds romberg toggle for integral computation + stores PDF node values. |
| projects/distributions/private/pybindings/distributions.cxx | Exposes new distributions to Python and updates TabulatedFluxDistribution constructors. |
| projects/distributions/private/primary/energy_direction/Tabulated2DFluxDistribution.cxx | Implements table loading, normalization, and MH sampling for (E, zenith). |
| projects/distributions/private/primary/energy_direction/PrimaryEnergyDirectionDistribution.cxx | Implements Sample() and density variables for the new base class. |
| projects/distributions/private/primary/energy/TabulatedFluxDistribution.cxx | Implements trapezoid-based integral option and stores PDF nodes. |
| projects/distributions/CMakeLists.txt | Adds new sources to the build. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (fluxTable.IsLogY()) { | ||
| double logZenithMin = log10(zenithMin); | ||
| double logZenithMax = log10(zenithMax); | ||
| zenith = pow(10,logZenithMin + u2 * (logZenithMax - logZenithMin)); | ||
| } | ||
| else { | ||
| zenith = zenithMin + u2 * (zenithMax - zenithMin); | ||
| } |
There was a problem hiding this comment.
ComputeIntegral() only applies the log-Jacobian for log-X, but SampleTablePoint() also supports log-Y. If fluxTable.IsLogY() is ever true, integral (and therefore pdf()) will be mis-normalized relative to sampling. Fix by applying the corresponding log-space change of variables in ComputeIntegral() for Y as well (or remove/forbid log-Y sampling if not supported)."
| if (fluxTable.IsLogY()) { | |
| double logZenithMin = log10(zenithMin); | |
| double logZenithMax = log10(zenithMax); | |
| zenith = pow(10,logZenithMin + u2 * (logZenithMax - logZenithMin)); | |
| } | |
| else { | |
| zenith = zenithMin + u2 * (zenithMax - zenithMin); | |
| } | |
| // Keep Y sampling in linear space so it matches the current integral | |
| // normalization. Log-Y sampling requires a corresponding Jacobian in | |
| // ComputeIntegral(), which is not applied here. | |
| zenith = zenithMin + u2 * (zenithMax - zenithMin); |
| // metropolis hastings variables | ||
| const size_t burnin = 40; // burnin parameter for MH sampling | ||
| mutable size_t MH_sampled_points = 0; // to track the number of samples | ||
| mutable double MH_density; |
There was a problem hiding this comment.
MH_density is never initialized, but it is used as a divisor in Metropolis-Hastings (odds = test_density / MH_density). This is undefined behavior and can produce NaNs/inf or random acceptance. Initialize MH_density (e.g., to 0.0) in the declaration or constructors, and consider resetting MH_density/MH_sampled_points when bounds change (SetEnergyBounds/SetZenithBounds) so the chain doesn’t continue with stale state after re-normalization.
| mutable double MH_density; | |
| mutable double MH_density = 0.0; |
| std::stringstream ss(buf); | ||
| double x, y, f; | ||
| ss >> x >> y >> f; |
There was a problem hiding this comment.
This translation unit uses std::stringstream and assert but does not include <sstream> or <cassert>. This can fail to compile depending on transitive includes. Add the missing standard headers in this file.
| assert(energies.size()==flux.size()); | ||
| assert(zeniths.size()==flux.size()); |
There was a problem hiding this comment.
This translation unit uses std::stringstream and assert but does not include <sstream> or <cassert>. This can fail to compile depending on transitive includes. Add the missing standard headers in this file.
| double simpsonIntegrate2D(const FuncType& func, double a1, double b1, double a2, double b2, | ||
| double tol=1e-3, const unsigned int N1 = 10, const unsigned int N2=10, const unsigned int maxIter=15){ |
There was a problem hiding this comment.
The convergence test divides by estimate, which becomes a division-by-zero when the integral is 0 (or extremely small), yielding inf/NaN and preventing correct convergence. Use an absolute error check (e.g., abs(estimate - prev_estimate) < tol_abs) or a mixed abs/relative criterion that guards estimate == 0. Also, Simpson’s rule requires an even number of subintervals per dimension; currently callers can pass odd N1/N2 and the weight pattern becomes invalid—validate/enforce even N1 and N2 (or document and adjust them internally).
| if(std::abs((estimate-prev_estimate)/estimate) < tol) { | ||
| return estimate; | ||
| } |
There was a problem hiding this comment.
The convergence test divides by estimate, which becomes a division-by-zero when the integral is 0 (or extremely small), yielding inf/NaN and preventing correct convergence. Use an absolute error check (e.g., abs(estimate - prev_estimate) < tol_abs) or a mixed abs/relative criterion that guards estimate == 0. Also, Simpson’s rule requires an even number of subintervals per dimension; currently callers can pass odd N1/N2 and the weight pattern becomes invalid—validate/enforce even N1 and N2 (or document and adjust them internally).
| inline double trapezoidIntegrate(std::vector<double>& x, std::vector<double>& y){ | ||
| if(x.size()!=y.size()) | ||
| throw(std::runtime_error("Integration x and y vectors must be the same size")); |
There was a problem hiding this comment.
trapezoidIntegrate does not modify x or y, but takes them by non-const reference, which unnecessarily restricts callers (can’t pass temporaries/const vectors) and obscures intent. Change the signature to take const std::vector<double>& for both. Additionally, consider throwing when x.size() < 2 to avoid silently returning 0 for empty/1-point inputs.
| inline double trapezoidIntegrate(std::vector<double>& x, std::vector<double>& y){ | |
| if(x.size()!=y.size()) | |
| throw(std::runtime_error("Integration x and y vectors must be the same size")); | |
| inline double trapezoidIntegrate(const std::vector<double>& x, const std::vector<double>& y){ | |
| if(x.size()!=y.size()) | |
| throw(std::runtime_error("Integration x and y vectors must be the same size")); | |
| if(x.size() < 2) | |
| throw(std::runtime_error("Integration requires at least two points")); |
| void TabulatedFluxDistribution::ComputeIntegral(bool romberg) { | ||
| std::function<double(double)> integrand = [&] (double x) -> double { | ||
| return unnormed_pdf(x); | ||
| }; | ||
| integral = siren::utilities::rombergIntegrate(integrand, energyMin, energyMax); | ||
| if (romberg) { | ||
| integral = siren::utilities::rombergIntegrate(integrand, energyMin, energyMax); | ||
| } else { | ||
| integral = siren::utilities::trapezoidIntegrate(energy_nodes, pdf_nodes); | ||
| } | ||
| } |
There was a problem hiding this comment.
When romberg == false, the integral ignores energyMin/energyMax entirely and integrates over all energy_nodes. This produces incorrect normalization if bounds are set narrower than the full table. Fix by trapezoid-integrating only over the portion of nodes within [energyMin, energyMax] (and, if needed, interpolating endpoints). Also ensure the selected integration method remains consistent after SetEnergyBounds()—currently SetEnergyBounds() calls ComputeIntegral() without passing the romberg choice (so it will revert to the default). Persist the method as a member or plumb it through all recompute sites.
| .def(init<std::string, bool, bool>()) | ||
| .def(init<double, double, std::string, bool, bool>()) | ||
| .def(init<std::vector<double>, std::vector<double>, bool, bool>()) | ||
| .def(init<double, double, std::vector<double>, std::vector<double>, bool, bool>()) |
There was a problem hiding this comment.
The Python API for TabulatedFluxDistribution appears to have become strictly more required-args than before (previously (filename, has_physical_normalization) etc.). Even though C++ provides defaults, these init<...>() overloads require callers to pass both booleans explicitly, which is a breaking change for existing Python users. Consider adding overloads matching the previous signatures, or bind with py::arg defaults so existing code continues to work while still exposing the new romberg toggle.
| .def(init<std::string, bool, bool>()) | |
| .def(init<double, double, std::string, bool, bool>()) | |
| .def(init<std::vector<double>, std::vector<double>, bool, bool>()) | |
| .def(init<double, double, std::vector<double>, std::vector<double>, bool, bool>()) | |
| .def(init<std::string, bool, bool>(), | |
| arg("filename"), | |
| arg("has_physical_normalization") = false, | |
| arg("romberg") = false) | |
| .def(init<double, double, std::string, bool, bool>(), | |
| arg("energy_min"), | |
| arg("energy_max"), | |
| arg("filename"), | |
| arg("has_physical_normalization") = false, | |
| arg("romberg") = false) | |
| .def(init<std::vector<double>, std::vector<double>, bool, bool>(), | |
| arg("energy_nodes"), | |
| arg("flux_nodes"), | |
| arg("has_physical_normalization") = false, | |
| arg("romberg") = false) | |
| .def(init<double, double, std::vector<double>, std::vector<double>, bool, bool>(), | |
| arg("energy_min"), | |
| arg("energy_max"), | |
| arg("energy_nodes"), | |
| arg("flux_nodes"), | |
| arg("has_physical_normalization") = false, | |
| arg("romberg") = false) |
4a078b6 to
4500aec
Compare
496977b to
967cc0a
Compare
4500aec to
e4231d5
Compare
967cc0a to
9204f13
Compare
e4231d5 to
05bc3dd
Compare
b717fce to
c04ebb4
Compare
05bc3dd to
c967cc7
Compare
|
Closing to reopen from the commit author's account so they can be added as a reviewer. Branch unchanged; will be re-linked from the new PR shortly. |
Stack: PR 7 of 14
This PR is part of a 14-PR stack decomposing
dev/HNL_DISinto reviewable chunks.feat/primary-external-distributionfeat/2d-flux-tablesMerge order:
feat/primary-external-distributionshould land before this PR.Squashed contents
Squashed diff for paths:
Source commits on dev/HNL_DIS_clean that contributed:
21451da (Nicholas Kamp): first attempt at external list primary injector
455b073 (Nicholas Kamp): introduce primary bounded and physical vertex distributions to sample vertex in the case that the inital position is already provided from the external distribution
521695f (Nicholas Kamp): fix issues with 2D tabular integral calculation, sampling errors with very short decay lengths, DarkNews errors, and kinematic erros in Dipole portal HNL DIS
73bc119 (Nicholas Kamp): sphere build fixes
82b4fdc (Nicholas Kamp): add ability to trapezoid integrate tabulated 1D fluxes
9098b42 (Nicholas Kamp): fix weighting issue in PrimaryExternalDist
9968ff7 (Nicholas Kamp): Start implementing 2D flux table for atmospheric fluxes
9dae77a (Nicholas Kamp): spherical volume position distribution
a79d440 (Nicholas Kamp): getting HNLs to work for SINE and UNDINE
a9aebd3 (Nicholas Kamp): fix log integration
c7cff6c (Nicholas Kamp): fixing build and runtime errors on 2D flux distribution
Notes
dev/HNL_DIS_clean..fitsdata files have been removed from the branch and are packaged separately.