@@ -26,7 +26,7 @@ def sample(self, rng: Generator) -> float: # type: ignore
2626
2727 Returns
2828 -------
29- float:
29+ float
3030 The sampled angle in radians
3131 """
3232 pass
@@ -74,7 +74,79 @@ def sample(self, rng: Generator) -> float:
7474
7575 Returns
7676 -------
77- float:
77+ float
7878 The sampled angle in radians
7979 """
8080 return np .arccos (rng .uniform (self .cos_angle_min , self .cos_angle_max ))
81+
82+
83+ class PolarArbitrary :
84+ """An arbitrary, finite-precision polar angular distribution in the CM frame
85+
86+ Define an arbitrary probability distribution for the polar angle using an
87+ array of uniformly-spaced angles and their probabilities, as well as the spacing
88+ of the angle values. We then sample from the distribution and smear the output angle
89+ within the spacing.
90+
91+ Note: If your distribution is defined using *any* of the pre-defined distributions,
92+ you should favor using those over PolarArbitrary. Sampling of arbitrary functions
93+ comes with a runtime performance penalty. That is: do not use this to sample from a
94+ uniform distribution.
95+
96+ Parameters
97+ ----------
98+ angles: numpy.ndarray
99+ The array of *lower* angle values. Each angle corresponds to a bin covering
100+ angle + bin_width. Angles should be in units of radians.
101+ probabilities: numpy.ndarray
102+ The probability of a given angle bin. Should sum to 1.0
103+ angle_bin_width: float
104+ The width of the angle bins in radians
105+
106+ Attributes
107+ ----------
108+ angle_width: float
109+ The angle bin width in radians
110+ probs: numpy.ndarray
111+ The probability of a given angle bin
112+ angles: numpy.ndarray
113+ The array of *lower* angle values. Each angle corresponds to a bin covering
114+ angle + bin_width. Angles should be in units of radians.
115+
116+ Methods
117+ -------
118+ sample(rng)
119+ Sample the distribution, returning a polar angle in radians
120+ """
121+
122+ def __init__ (
123+ self ,
124+ angles : np .ndarray ,
125+ probabilities : np .ndarray ,
126+ angle_bin_width : float ,
127+ ):
128+ if np .sum (probabilities ) > 1.0 :
129+ raise ValueError (
130+ f"The sum of the probabilities passed to PolarArbitrary should be 1.0. Yours sum to { np .sum (probabilities )} "
131+ )
132+ self .angle_width = angle_bin_width
133+ self .probs = probabilities
134+ self .angles = angles
135+
136+ def sample (self , rng : Generator ) -> float :
137+ """Sample the distribution, returning a polar angle in radians
138+
139+ Parameters
140+ ----------
141+ rng: numpy.random.Generator
142+ The random number generator
143+
144+ Returns
145+ -------
146+ float
147+ The sampled angle in radians
148+ """
149+ return (
150+ rng .choice (self .angles , p = self .probs )
151+ + rng .uniform (0.0 , 1.0 ) * self .angle_width
152+ )
0 commit comments