Add Diffuse Supernova Neutrino Background (DSNB) flux calculation module - #453
dristii-32 wants to merge 11 commits into
Conversation
|
Hello Dristii, welcome to SNEWPY! Before we go into the details of this code, my first question is at a much higher level: As far as I can tell, this code has really no connection to the existing SNEWPY package, right? (Apart from being in the same area of astrophysics, of course.) So to me, it's not at all clear what advantages there would be to including this as part of SNEWPY? |
|
Thanks for the welcome and the question. The motivation for this was that the DSNB is the natural diffuse equivalent of the time dependent supernova neutrino fluxes already supported by SNEWPY. SNEWPY currently includes interfaces to supernova neutrino emission models and tools for computing the observable fluxes and event rates from individual core-collapse supernovae. But many neutrino experiments (Super-Kamiokande with Gd, JUNO, future detectors etc) also look at the diffuse supernova neutrino background, which is obtained by summing the contribution from the whole cosmic population of core-collapse supernovae. The goal of this module is thus to expand SNEWPY from single-supernova calculations to the cosmological DSNB, so that users can stay within the same software ecosystem when transition from burst-neutrino calculations to DSNB predictions. My hope was that this would provide a common framework where users could: compute DSNB fluxes with the same physical conventions as SNEWPY, eventually use SNEWPY supernova emission models as DSNB source spectra directly rather than relying on fixed analytical spectra, compare burst-neutrino and diffuse-neutrino predictions within one package. If the maintainers think the current implementation is too independent from SNEWPY's existing architecture, I am happy to discuss redesigning it so that it interfaces directly with SNEWPY models, rather than existing as a largely standalone module. |
|
Hi Dristii, Adding the capability to SNEWPY to compute the DSNB event rate in a detector is something that has been mentioned before so I am happy to see this contribution. I had a quick look through the code. My first impression is the same as Jost's: the overlap between dsnb.py and the rest of SNEWPY could be much greater than at the present time. There are two places in particular where I think the integration between them could be tighter: first in the supernova neutrino spectra, and second in using SNEWPY's RateCalculator (or it's interface with SNOwGLoBES) for the DSNB event rate calculation in more detectors and more detector channels. I have some thoughts on how to improve the integration but perhaps you have already better ideas. |
|
Thank you for the feedback. I would very much like to hear your thoughts on how to improve the integration. The two directions you mentioned (tighter coupling with SNEWPY's emission models and with RateCalculator) are what I had in mind as next steps. I am thinking that; For the emission spectra: modify DSNBFlux to accept any SNEWPY supernova model (e.g. Nakazato_2013) as the sn_model argument, time-integrate its luminosity to obtain dN/dE per supernova, and use the current analytical PinchedSpectrum only as the default. This would mean a user could directly pass in a simulation-based spectrum rather than a parametric approximation. For the event rate: replace the custom IBDCrossSection and integrated_ibd_rate with a call to SNEWPY's RateCalculator, inheriting support for all detector channels and materials already implemented there. I can adapt the implementation around whichever interface fits best with SNEWPY's existing design. Any guidance on the intended architecture would be very helpful, and I’d be glad to work on a revised version based on your suggestions. |
|
Those sound like the ideas I was going to suggest. You can find many scripts and notebooks in the doc directory that show how to use the supernova models and the RateCalculator. Here's one that might be useful https://github.com/SNEWS2/snewpy/blob/jpkneller-add_collate_for_RateCalculator/doc/source/nb/Analytic3Species.ipynb |
|
Thank you for pointing to the Analytic3Species and Warren_2020 notebooks. Based on your feedback, I am working on a revised version with two concrete improvements:
Could you advise on the right interface, should DSNBFlux.integrated_ibd_rate() be replaced with a wrapper around RateCalculator, or would it be cleaner to keep the current analytic IBD rate as a fast default and offer RateCalculator as an optional backend? I will push the revised code in the next few days. |
|
What would be the use case for that? We know that the DSNB spectrum will not be equivalent to the spectrum from any one progenitor; it is an IMF-weighted average over progenitor spectra with additional modifications (e.g., due to redshift). Existing spectra published in the literature take these into account to various degrees—they may still make unphysical assumptions, but at least those are described and motivated in the corresponding paper, and the limitations are generally well understood. Building an ad hoc spectrum based on an individual progenitor simulation lacks all that; so I don’t see a meaningful use case. Without a clear use case, such a feature adds unnecessary complexity to the implementation and API—meaning more work for us and for users, as well as the potential for accidental misuse if inexperienced users don’t understand the difference between this and 2).
This sounds good to me! Regarding the IMF: This is outside of my area of expertise, but I thought the differences between various IMFs are only in the low-mass region? If different IMFs all agree in the high-mass region relevant for SNe, do we actually need an option for custom IMFs? I’d probably prefer to drop this to keep the implementation and API simpler.
I haven’t thought about the architecture yet; so I can’t comment on the design of the interface. But just as a general comment: I would like to avoid a situation where snewpy contains multiple different implementations of the same cross section. If the IBD implementation in |
|
Thank you for the detailed feedback. I will revise the PR as follows: Removing: from_snewpy_model() for a single progenitor; agreed there is no physically meaningful use case for the DSNB. Simplifying: from_snewpy_model_collection(); will retain this with a fixed Salpeter (1955) IMF and remove the custom IMF option to keep the API clean. Adding: DSNBFlux.to_fluence(energy, exposure); wraps the computed DSNB flux as a SNEWPY Fluence object that can be passed directly to RateCalculator.run(). This delegates all cross-section and detector calculations to the existing SNEWPY infrastructure, avoiding any duplicate implementations. I will push the revised code shortly. |
|
Hello, the revised code addresses all points raised: IBDCrossSection and the duplicate cross-section implementation. ibd_rate() and integrated_ibd_rate() are gone. from_snewpy_model() for a single progenitor is removed. from_snewpy_model_collection() now uses a fixed Salpeter (1955) IMF only. The custom IMF option is dropped. DSNBFlux.to_fluence(energy, exposure) wraps the computed DSNB flux as a snewpy.flux.Fluence object. Users pass this directly to RateCalculator.run(): python This delegates all cross-section and detector calculations to the existing SNEWPY infrastructure with no duplicate implementations. I wanted to ask that should DSNBFlux live at snewpy.dsnb (a new top-level module), or would you prefer it nested somewhere else in the package hierarchy, for example snewpy.models.dsnb? I will be glad to move it wherever fits best before this goes in. |
JostMigenda
left a comment
There was a problem hiding this comment.
It’s late on Friday evening for me, so I haven’t looked in detail yet, but here are a couple of comments on my first scroll through. Many are fairly minor bits of cleanup, but there are a few open physics questions/issues that require further discussion.
| __all__ = [ | ||
| "CoreCollapseRate", | ||
| "PinchedSpectrum", | ||
| "IBDCrossSection", |
There was a problem hiding this comment.
| "IBDCrossSection", |
Some cleanup. (Yep, forgetting to update __all__ happens to me regularly, too … 😅)
| # Internal physical constants — PDG 2022 | ||
| # All in MeV / cm / s to avoid repeated unit conversions in inner loops. | ||
| # --------------------------------------------------------------------------- |
There was a problem hiding this comment.
I think these were only used for the IBD implementation?
| try: | ||
| from snewpy.neutrino import Flavor as _Flavor | ||
| except ImportError as exc: | ||
| raise ImportError( | ||
| "snewpy must be installed to use SNEWPYSpectrum. " | ||
| "Install it with: pip install snewpy" | ||
| ) from exc |
There was a problem hiding this comment.
This looks like a leftover from the first, stand-alone version of the code. Can you clean this up?
(As well as at least one other instance of this, later in the file.)
There was a problem hiding this comment.
Also, since we know that snewpy.neutrino.Flavor exists, that export can move to the top of the file and we can set the default value of flavor in the definition right away, rather than checking for None in the function body.
| -------- | ||
| >>> import numpy as np | ||
| >>> import astropy.units as u | ||
| >>> from snewpy_dsnb.dsnb import DSNBFlux |
There was a problem hiding this comment.
| >>> from snewpy_dsnb.dsnb import DSNBFlux | |
| >>> from snewpy.dsnb import DSNBFlux |
| masses = np.array([m for _, m in models_with_masses], dtype=float) | ||
| weights = salpeter_imf(masses) | ||
| weights = weights / weights.sum() |
There was a problem hiding this comment.
I think the weighting needs to be more complicated than this, in the general case. In this example, the 13 Msol progenitor would have a weight of 13^-2.35/(13^-2.35+20^-2.35) = 0.733. Now imagine that I accidentally add the 20 Msol progenitor to pairs twice—that would suddenly decrease the 13 Msol progenitor’s weight to 13^-2.35/(13^-2.35+2*20^-2.35) = 0.579.
(I’m wondering if a {mass: model} dictionary might be a slightly nicer data type here than a list of (model, mass) tuples? In part because it removes a level of nesting and in part because it eliminates that risk of accidental duplicates.)
Additionally, similar issues arise if I were to add a lot of progenitors in a small mass interval—that mass interval would suddenly have a much higher weight. Instead, I suspect the weight should be based on the integrated area under the IMF distribution that each progenitor represents? (Just my first guess; we should think this through more thoroughly than I can do on a Friday evening.)
| def from_snewpy_model_collection(cls, models_with_masses, flavor=None, | ||
| imf=None, f_bh=0.27, **kwargs): | ||
| """ | ||
| IMF-weighted average spectrum over multiple SNEWPY models. |
There was a problem hiding this comment.
It looks like this is an accidental duplicate?
| from snewpy.flux import Fluence | ||
| from snewpy.neutrino import Flavor | ||
|
|
||
| if flavor is None: | ||
| flavor = Flavor.NU_E_BAR |
There was a problem hiding this comment.
Similar to the comment above: The exports can move to the top of the file and we can set the default value of flavor in the definition right away, rather than checking for None in the function body.
| def smeared_flux( | ||
| self, | ||
| energy: u.Quantity, | ||
| energy_resolution: float, | ||
| ) -> u.Quantity: | ||
| """ | ||
| Flux convolved with a Gaussian detector energy resolution. |
There was a problem hiding this comment.
This is no longer used, I think.
| class SNEWPYSpectrum: | ||
| """ | ||
| SN neutrino emission spectrum derived from a SNEWPY simulation model. |
There was a problem hiding this comment.
This looks like a duplicate; class SNEWPYSpectrum is already defined further up in this file? Though I’m not sure if there are differences between the two (and if so, which is the correct one).
Clean up per PR SNEWS2#453 review: remove IBDCrossSection refs, move Flavor import to top level, fix function signatures, remove smeared_flux
Co-authored-by: Jost Migenda <jost.migenda@kcl.ac.uk>
Co-authored-by: Jost Migenda <jost.migenda@kcl.ac.uk>
|
Hello, all 21 tests now pass. Summary of changes:
|
|
Hi, just checking in.. Happy to make any further changes if needed... Let me know if there is anything else before this is ready for merge. |
|
@dristii-32, Sorry for the delay in getting back to you. My personal preference is that the DSNB flux becomes a model class similar to, say, Bollig_2016 or Warren_2020. Method names for the DSNB model should be identical to those for all the other models (in those cases where it makes sense to have similar methods) so that the model is intuitive for users. We definitely need a usage example which might be along the lines of: Perhaps @JostMigenda has ideas too. The model = DSNB(Warren_2020) is meant to be suggestive: more parameters might need to be supplied or maybe the input to the class is a list of supernova model instances the user has previously created along with a way to weight them. Is that clear? Perhaps I don't fully understand the code but as Jost said before, when I look through dsnb.py I can see a lot of code which looks to a) duplicate existing parts of SNEWPY and b) does not integrate well with SNEWPY because it does not use the essential components such as the Container for spectra / fluxes / fluences. In particular, I am confused by the need for the SNEWPYSpectrum and PinchedSpectrum classes. SNEWPYSpectrum appears to time integrate the spectrum (shouldn't it time average?) but only for a single flavor and without a flavor transformation prescription. It also only works for pinched models. This same functionality (but without the limitations) can be achieved with spectra = model.get_flux and spectra.integrate('time') as in the example above. PinchedSpectrum is also a very similar to SNEWPY's existing model class PinchedModel. |
|
Thank you for the redesign direction. I understand that I should
Before I implement, I wanted to ask that for DSNB(Warren_2020) where Warren_2020 is the class; should the code automatically enumerate all available progenitor parameter combinations from Warren_2020.param and apply IMF weighting by progenitor mass...? Or is the preferred interface DSNB([instance1, instance2, ...]) where the user creates the instances themselves? The first is more convenient but requires knowing which parameter key corresponds to progenitor mass (which varies across model classes). The second is more explicit. Or maybe I can also implement a combination where passing a class triggers the auto-enumeration and passing a list uses those instances directly. I will begin the rewrite once this is clear. |
From what I can see, PinchedSpectrum and SNEWPYSpectrum duplicate existing code or existing methods. CoreCollapseRate can be kept.
Yes, I would suggest that option so that the interface with the DSNB model is most similar to any supernova model.
I agree with you: my preference is also for the second option because it gives the user the greatest flexibility but I can see that it might make the code more complicated. There is still the potential for a problem if more than one model in the list has the same mass. I was not aware that the progenitor mass key was different across the models: we will fix that. For now use the key for the Warren_2020 models. |
|
Hello, the revised code has been pushed. The module now has a single DSNB class with get_flux(t, E, flavor_xform) as the primary method, matching the interface of other SNEWPY model classes. PinchedSpectrum and SNEWPYSpectrum are removed; the per-supernova emission spectrum is computed directly from each SNEWPY model's luminosity, meanE, and pinch time series via time integration. CoreCollapseRate is kept. Usage now follows this pattern: python Also, flavor_xform is currently reserved for future use. The NU_E_BAR component carries the full DSNB flux since MSW effects average out over cosmological distances for the standard calculation. |
|
Great! I'll take a look. |
Apply MSW oscillation effects per-progenitor before the cosmological integration. flavor_xform now uses the SNEWPY prob_eebar/prob_xebar API (AdiabaticMSW). Fixes Flavor enum (NU_X_BAR), flux array access (.array), and MassHierarchy instantiation. NH/IH ratio matches Lunardini & Tamborra (2012).
|
Thank you for the pointer. The revised code now,
Also, AdiabaticMSW.prob_eebar returns a scalar independent of (t, E), so _oscillation_probs queries it once at a reference point. If a time/energy-dependent transformation is needed in the future, _dNdE_per_sn would need to evaluate probabilities on the full (N_t, N_E) grid. I will be happy to add that if required. Tests confirm NH/IH ratio and the ~50–60% MSW effect on the NU_E_BAR flux, consistent with Lunardini & Tamborra (2012). |
Summary
This PR adds a new
snewpy.dsnbmodule for computing the Diffuse Supernova Neutrino Background (DSNB) electron antineutrino flux and inverse beta decay (IBD) event rates.The implementation follows:
Features
Testing
The module imports successfully within SNEWPY.
Added a dedicated test suite:
All tests pass:
Notes
This contribution is based on an independently developed and tested implementation that has also been released as a standalone Python package on PyPI. The code has been adapted here for integration into SNEWPY.