• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ObservationalExpansions / flex / 28375224463

29 Jun 2026 01:22PM UTC coverage: 95.699% (-4.3%) from 100.0%
28375224463

Pull #3

github

web-flow
Merge 99b14cae5 into 7b81bf3f6
Pull Request #3: add a normalization test

69 of 77 new or added lines in 2 files covered. (89.61%)

178 of 186 relevant lines covered (95.7%)

0.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.84
/src/flex/flexbase.py
1
from numbers import Integral, Real
1✔
2

3
import numpy as np
1✔
4

5
# for the Laguerre polynomials
6
from scipy.special import eval_genlaguerre
1✔
7

8

9
def _as_1d_numeric_array(name, value):
1✔
10
    try:
1✔
11
        array = np.asarray(value, dtype=float)
1✔
NEW
12
    except (TypeError, ValueError) as exc:
×
NEW
13
        raise ValueError(f"{name} must be a numeric array-like structure.") from exc
×
14

15
    if array.ndim != 1:
1✔
NEW
16
        raise ValueError(f"{name} must be a one-dimensional array-like structure.")
×
17

18
    return array
1✔
19

20

21
def _as_scalar_or_matching_array(name, value, shape):
1✔
22
    if isinstance(value, bool):
1✔
NEW
23
        raise ValueError(f"{name} must be a scalar or array-like structure.")
×
24

25
    if isinstance(value, Real):
1✔
26
        return value
1✔
27

28
    try:
1✔
29
        array = np.asarray(value, dtype=float)
1✔
NEW
30
    except (TypeError, ValueError) as exc:
×
NEW
31
        raise ValueError(f"{name} must be a scalar or numeric array-like structure.") from exc
×
32

33
    if array.ndim == 0:
1✔
NEW
34
        return array.item()
×
35
    if array.shape != shape:
1✔
36
        raise ValueError(f"{name} must be scalar or have the same shape as R.")
1✔
37

38
    return array
1✔
39

40

41
class FLEX:
1✔
42
    """
43
   FLEX class for calculating Laguerre basis amplitudes.
44

45
    This class provides methods for calculating Laguerre basis amplitudes based on Weinberg & Petersen (2021).
46

47
    Parameters:
48
        rscl (float): Scale parameter for the Laguerre basis.
49
        mass (array-like): Mass values for particles.
50
        phi (array-like): Angular phi values.
51
        velocity (array-like): Velocity values.
52
        R (array-like): Radial values.
53
        mmax (int): Maximum order parameter for m.
54
        nmax (int): Maximum order parameter for n.
55

56
    Methods:
57
        gamma_n(nrange, rscl): Calculate the Laguerre alpha=1 normalisation.
58
        G_n(R, nrange, rscl): Calculate the Laguerre basis.
59
        n_m(): Calculate the angular normalisation.
60
        laguerre_amplitudes(): Calculate Laguerre amplitudes for the given parameters.
61
        laguerre_reconstruction(rr, pp): Calculate Laguerre reconstruction.
62

63
    Attributes:
64
        rscl (float): Scale parameter for the Laguerre basis.
65
        mass (array-like): Mass values for particles.
66
        phi (array-like): Angular phi values.
67
        velocity (array-like): Velocity values.
68
        R (array-like): Radial values.
69
        mmax (int): Maximum order parameter for m.
70
        nmax (int): Maximum order parameter for n.
71
        coscoefs (array-like): Cosine coefficients.
72
        sincoefs (array-like): Sine coefficients.
73
        reconstruction (array-like): Laguerre reconstruction result.
74
    """
75

76
    def __init__(self, rscl, mmax, nmax, R, phi, mass=1., velocity=1., newaxis=False):
1✔
77
        """
78
        Initialize the LaguerreAmplitudes instance with parameters.
79

80
        Args:
81
            rscl (float): Scale parameter for the Laguerre basis.
82
            mmax (int): Maximum Fourier harmonic order.
83
            nmax (int): Maximum Laguerre order.
84
            R (array-like): Radial values.
85
            velocity (array-like): Velocity values.
86
            mass (integer or array-like): Mass values for particles.
87
            phi (integer or array-like): Angular phi values.
88

89
        """
90

91
        # check for input validity
92
        if not isinstance(rscl, Real) or isinstance(rscl, bool):
1✔
93
            raise ValueError("rscl must be a scalar value.")
1✔
94
        if not isinstance(mmax, Integral) or isinstance(mmax, bool) or mmax < 0:
1✔
95
            raise ValueError("mmax must be a non-negative integer.")
1✔
96
        if not isinstance(nmax, Integral) or isinstance(nmax, bool) or nmax < 0:
1✔
97
            raise ValueError("nmax must be a non-negative integer.")
1✔
98
        if not isinstance(R, (np.ndarray, list, tuple)):
1✔
99
            raise ValueError("R must be an array-like structure.")
1✔
100
        if not isinstance(phi, (np.ndarray, list, tuple)):
1✔
101
            raise ValueError("phi must be an array-like structure.")
1✔
102
        if not isinstance(mass, (Real, np.ndarray, list, tuple)):
1✔
103
            raise ValueError("mass must be a scalar or array-like structure.")
1✔
104
        if not isinstance(velocity, (Real, np.ndarray, list, tuple)):
1✔
105
            raise ValueError("velocity must be a scalar or array-like structure.")
1✔
106

107
        R = _as_1d_numeric_array("R", R)
1✔
108
        phi = _as_1d_numeric_array("phi", phi)
1✔
109
        if phi.shape != R.shape:
1✔
110
            raise ValueError("phi must have the same shape as R.")
1✔
111

112
        mass = _as_scalar_or_matching_array("mass", mass, R.shape)
1✔
113
        velocity = _as_scalar_or_matching_array("velocity", velocity, R.shape)
1✔
114

115
        self.rscl     = rscl
1✔
116
        self.mmax     = mmax
1✔
117
        self.nmax     = nmax
1✔
118
        self.R        = R
1✔
119
        self.phi      = phi
1✔
120
        self.mass     = mass
1✔
121
        self.velocity = velocity
1✔
122

123
        # run the amplitude calculation
124
        if newaxis:
1✔
125
            self.laguerre_amplitudes_newaxis()
1✔
126
        else:
127
            # default behaviour 
128
            self.laguerre_amplitudes()
1✔
129

130
    def _gamma_n(self, nrange, rscl):
1✔
131
        """
132
        Calculate the Laguerre alpha=1 normalisation.
133

134
        Args:
135
            nrange (array-like): Range of order parameters.
136
            rscl (float): Scale parameter for the Laguerre basis.
137

138
        Returns:
139
            array-like: Laguerre alpha=1 normalisation values.
140
        """
141
        return (rscl / 2.) * np.sqrt(nrange + 1.)
1✔
142

143
    def _G_n(self, R, nrange, rscl):
1✔
144
        """
145
        Calculate the Laguerre basis.
146

147
        Args:
148
            R (array-like): Radial values.
149
            nrange (array-like): Range of order parameters.
150
            rscl (float): Scale parameter for the Laguerre basis.
151

152
        Returns:
153
            array-like: Laguerre basis values.
154
        """
155
        laguerrevalues = np.array([eval_genlaguerre(n, 1, 2 * R / rscl)/self._gamma_n(n, rscl) for n in nrange])
1✔
156
        return np.exp(-R / rscl) * laguerrevalues
1✔
157

158
    def _n_m(self):
1✔
159
        """
160
        Calculate the angular normalisation.
161

162
        Returns:
163
            array-like: Angular normalisation values.
164
        """
165
        deltam0 = np.zeros(self.mmax+1)
1✔
166

167
        deltam0[0] = 1.0
1✔
168

169
        return np.power((deltam0 + 1) * np.pi / 2.,-1.)
1✔
170

171
    def laguerre_amplitudes_newaxis(self):
1✔
172
        """
173
        Calculate Laguerre amplitudes for the given parameters.
174

175
        Returns:
176
            tuple: Tuple containing the cosine and sine amplitudes.
177
        """
178

179
        G_j = self._G_n(self.R, np.arange(0, self.nmax, 1), self.rscl)
1✔
180

181
        nmvals = self._n_m()
1✔
182
        mrange = np.arange(0, self.mmax + 1, 1)
1✔
183
        cosm = nmvals[:, np.newaxis] * np.cos(mrange[:, np.newaxis] * self.phi)
1✔
184
        sinm = nmvals[:, np.newaxis] * np.sin(mrange[:, np.newaxis] * self.phi)
1✔
185

186
        # broadcast to sum values
187
        self.coscoefs = np.nansum(
1✔
188
            cosm[:, np.newaxis, :] * G_j[np.newaxis, :, :] * self.mass * self.velocity,
189
            axis=2,
190
        )
191
        self.sincoefs = np.nansum(
1✔
192
            sinm[:, np.newaxis, :] * G_j[np.newaxis, :, :] * self.mass * self.velocity,
193
            axis=2,
194
        )
195

196

197
    def laguerre_amplitudes(self):
1✔
198
        """
199
        Calculate Laguerre amplitudes for the given parameters.
200

201
        Returns:
202
            none. Attributes are added containing the cosine and sine amplitudes.
203
        """
204

205
        G_j = self._G_n(self.R, np.arange(0, self.nmax, 1), self.rscl)
1✔
206

207
        nmvals = self._n_m()
1✔
208
        mrange = np.arange(0, self.mmax + 1, 1)
1✔
209
        cosm = nmvals[:, np.newaxis] * np.cos(mrange[:, np.newaxis] * self.phi)
1✔
210
        sinm = nmvals[:, np.newaxis] * np.sin(mrange[:, np.newaxis] * self.phi)
1✔
211

212
        if np.isscalar(self.mass) and np.isscalar(self.velocity):
1✔
213
            scale = self.mass * self.velocity  # scalar
1✔
214
            self.coscoefs = scale * np.einsum('mn,jn->mj', cosm, G_j)
1✔
215
            self.sincoefs = scale * np.einsum('mn,jn->mj', sinm, G_j)   
1✔
216
        else:
217
            # vector case
218
            self.coscoefs = np.einsum('mn,jn,n->mj', cosm, G_j, self.mass * self.velocity)
1✔
219
            self.sincoefs = np.einsum('mn,jn,n->mj', sinm, G_j, self.mass * self.velocity)
1✔
220

221
    def laguerre_reconstruction(self, rr, pp):
1✔
222
        """
223
        Reconstruct a function using Laguerre amplitudes.
224

225
        Args:
226
            rr (array-like): Radial values.
227
            pp (array-like): Angular phi values.
228

229
        This method reconstructs a function using the Laguerre amplitudes calculated with the `laguerre_amplitudes` method.
230

231
        Returns:
232
            array-like: The reconstructed function values.
233
        """
234
        rr = _as_1d_numeric_array("rr", rr)
1✔
235
        pp = _as_1d_numeric_array("pp", pp)
1✔
236
        if pp.shape != rr.shape:
1✔
NEW
237
            raise ValueError("pp must have the same shape as rr.")
×
238

239
        G_j = self._G_n(rr, np.arange(0, self.nmax, 1), self.rscl)
1✔
240

241
        mrange = np.arange(0, self.mmax + 1, 1)
1✔
242
        cosm = np.cos(mrange[:, np.newaxis] * pp)
1✔
243
        sinm = np.sin(mrange[:, np.newaxis] * pp)
1✔
244

245
        cos_reconstruction = np.einsum('mn,mi,ni->i', self.coscoefs, cosm, G_j)
1✔
246
        sin_reconstruction = np.einsum('mn,mi,ni->i', self.sincoefs, sinm, G_j)
1✔
247

248
        self.reconstruction = 0.5 * (cos_reconstruction + sin_reconstruction)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc