Skip to content

Commit 3d3dbc3

Browse files
committed
Move some LLS from the numerically unstable normal matrix implementation, to the better SVD implementation.
Converted `fit_for_poly_coefs(...)`, `fit_for_poly_coefs(...)`, `fit_for_fullrangefraction_coefs(...)`, `fit_sqrt_poly_fwhm_lls(...)`, `fit_energy_cal_imp(...)`, `fit_from_channel_energies_imp(...)`, and maybe somewhere else. Added unit tests before converting, to be sure behaviour was the same after changing. Also added alternative call signatures for Berstein functions, to accomidate RelActAuto, in the future.
1 parent dcfd32f commit 3d3dbc3

File tree

9 files changed

+1439
-218
lines changed

9 files changed

+1439
-218
lines changed

InterSpec/BersteinPolynomial.hpp

Lines changed: 139 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -80,45 +80,69 @@ T binomial_coefficient( unsigned int n, unsigned int k )
8080
and C(n,i) is the binomial coefficient.
8181
8282
@param x The evaluation point, must be in the range [0, 1]
83-
@param coefficients Vector of Bernstein coefficients, size determines polynomial degree
83+
@param coefficients Pointer to array of Bernstein coefficients
84+
@param num_coefficients Number of coefficients, determines polynomial degree
8485
@return The evaluated polynomial value
8586
8687
Template parameter T can be double, float, or ceres::Jet<> types.
8788
*/
8889
template<typename T>
89-
T evaluate( const T& x, const std::vector<T>& coefficients )
90+
T evaluate( const T& x, const T * const coefficients, const size_t num_coefficients )
9091
{
91-
if( coefficients.empty() )
92+
if( num_coefficients == 0 )
9293
throw std::invalid_argument( "BersteinPolynomial::evaluate: coefficients cannot be empty" );
9394

94-
const unsigned int n = static_cast<unsigned int>(coefficients.size() - 1); // degree
95-
T result = T(0);
95+
const unsigned int n = static_cast<unsigned int>(num_coefficients - 1); // degree
96+
T result = T(0.0);
9697

97-
// For numerical stability, we'll compute using the recursive formula
98-
// B_{i,n}(x) = (1-x) * B_{i,n-1}(x) + x * B_{i-1,n-1}(x)
99-
// But for simplicity and given the expected low orders (up to 5-6),
100-
// we'll use the direct formula with binomial coefficients
98+
// Optimized evaluation using incremental power computation
99+
// This avoids O(n^2) power calculations while being cache-friendly
100+
const T one_minus_x = T(1.0) - x;
101101

102-
for( unsigned int i = 0; i <= n; ++i )
102+
// For small degrees (common case), use stack allocation to avoid heap overhead
103+
if( n <= 6 ) // Matches our precomputed binomial table
103104
{
104-
// Compute binomial coefficient C(n,i)
105-
const T binomial_coeff = binomial_coefficient<T>( n, i );
105+
T x_powers[7], one_minus_x_powers[7];
106106

107-
// Compute x^i
108-
T x_pow_i = T(1);
109-
for( size_t j = 0; j < i; ++j )
110-
x_pow_i *= x;
107+
x_powers[0] = T(1.0);
108+
one_minus_x_powers[0] = T(1.0);
111109

112-
// Compute (1-x)^{n-i}
113-
T one_minus_x_pow = T(1);
114-
const T one_minus_x = T(1) - x;
115-
for( size_t j = 0; j < (n - i); ++j )
116-
one_minus_x_pow *= one_minus_x;
110+
for( unsigned int i = 1; i <= n; ++i )
111+
{
112+
x_powers[i] = x_powers[i-1] * x;
113+
one_minus_x_powers[i] = one_minus_x_powers[i-1] * one_minus_x;
114+
}
117115

118-
// Add term to result
119-
result += coefficients[i] * binomial_coeff * x_pow_i * one_minus_x_pow;
116+
for( unsigned int i = 0; i <= n; ++i )
117+
{
118+
const T binomial_coeff = binomial_coefficient<T>( n, i );
119+
const T basis_value = binomial_coeff * x_powers[i] * one_minus_x_powers[n-i];
120+
result += coefficients[i] * basis_value;
121+
}
122+
}
123+
else
124+
{
125+
// For higher degrees, use the original nested loop approach to avoid heap allocation
126+
// This is rare in practice (comment mentions expected orders up to 5-6)
127+
for( unsigned int i = 0; i <= n; ++i )
128+
{
129+
const T binomial_coeff = binomial_coefficient<T>( n, i );
130+
131+
T x_pow_i = T(1);
132+
for( unsigned int j = 0; j < i; ++j )
133+
x_pow_i *= x;
134+
135+
T one_minus_x_pow = T(1);
136+
for( unsigned int j = 0; j < (n - i); ++j )
137+
one_minus_x_pow *= one_minus_x;
138+
139+
result += coefficients[i] * binomial_coeff * x_pow_i * one_minus_x_pow;
140+
}
120141
}
121142

143+
// TODO: Consider scalar power tracking optimization (x_power *= x, one_minus_x_power /= one_minus_x)
144+
// but need to handle edge cases like x=0, x=1 more carefully to avoid division by zero
145+
122146
return result;
123147
}
124148

@@ -130,23 +154,25 @@ T evaluate( const T& x, const std::vector<T>& coefficients )
130154
131155
The input x range [x_min, x_max] is mapped to [0, 1] for the Bernstein representation.
132156
133-
@param power_coeffs Power series coefficients [a_0, a_1, ..., a_n]
157+
@param power_coeffs Pointer to array of power series coefficients [a_0, a_1, ..., a_n]
158+
@param num_coefficients Number of power series coefficients
134159
@param x_min Minimum x value for the power series domain
135160
@param x_max Maximum x value for the power series domain
136161
@return Vector of Bernstein coefficients
137162
*/
138163
template<typename T>
139-
std::vector<T> power_series_to_bernstein( const std::vector<T>& power_coeffs,
164+
std::vector<T> power_series_to_bernstein( const T* power_coeffs,
165+
size_t num_coefficients,
140166
const T& x_min,
141167
const T& x_max )
142168
{
143-
if( power_coeffs.empty() )
169+
if( num_coefficients == 0 )
144170
throw std::invalid_argument( "BersteinPolynomial::power_series_to_bernstein: coefficients cannot be empty" );
145171

146172
if( x_max <= x_min )
147173
throw std::invalid_argument( "BersteinPolynomial::power_series_to_bernstein: x_max must be greater than x_min" );
148174

149-
const unsigned int n = static_cast<unsigned int>(power_coeffs.size() - 1); // degree
175+
const unsigned int n = static_cast<unsigned int>(num_coefficients - 1); // degree
150176
std::vector<T> bernstein_coeffs( n + 1, T(0) );
151177

152178
// First, transform the polynomial coefficients from domain [x_min, x_max] to [0, 1]
@@ -198,23 +224,25 @@ std::vector<T> power_series_to_bernstein( const std::vector<T>& power_coeffs,
198224
Converts from Bernstein representation: P(x) = sum_{i=0}^n b_i * B_{i,n}(x) defined on [0,1]
199225
to power series representation: P(x) = a_0 + a_1*x + a_2*x^2 + ... + a_n*x^n defined on [x_min, x_max]
200226
201-
@param bernstein_coeffs Bernstein coefficients [b_0, b_1, ..., b_n]
227+
@param bernstein_coeffs Pointer to array of Bernstein coefficients [b_0, b_1, ..., b_n]
228+
@param num_coefficients Number of Bernstein coefficients
202229
@param x_min Minimum x value for the output power series domain
203230
@param x_max Maximum x value for the output power series domain
204231
@return Vector of power series coefficients
205232
*/
206233
template<typename T>
207-
std::vector<T> bernstein_to_power_series( const std::vector<T>& bernstein_coeffs,
234+
std::vector<T> bernstein_to_power_series( const T* bernstein_coeffs,
235+
size_t num_coefficients,
208236
const T& x_min,
209237
const T& x_max )
210238
{
211-
if( bernstein_coeffs.empty() )
239+
if( num_coefficients == 0 )
212240
throw std::invalid_argument( "BersteinPolynomial::bernstein_to_power_series: coefficients cannot be empty" );
213241

214242
if( x_max <= x_min )
215243
throw std::invalid_argument( "BersteinPolynomial::bernstein_to_power_series: x_max must be greater than x_min" );
216244

217-
const unsigned int n = static_cast<unsigned int>(bernstein_coeffs.size() - 1); // degree
245+
const unsigned int n = static_cast<unsigned int>(num_coefficients - 1); // degree
218246

219247
// First convert Bernstein coefficients to power series coefficients on [0,1]
220248
// Use the inverse of the transformation matrix M where M[i][j] = C(i,j)/C(n,j) for j<=i
@@ -280,28 +308,30 @@ std::vector<T> bernstein_to_power_series( const std::vector<T>& bernstein_coeffs
280308
Bernstein polynomial.
281309
282310
@param x The evaluation point in [x_min, x_max]
283-
@param power_coeffs Power series coefficients
311+
@param power_coeffs Pointer to array of power series coefficients
312+
@param num_coefficients Number of power series coefficients
284313
@param x_min Minimum x value for the power series domain
285314
@param x_max Maximum x value for the power series domain
286315
@return The evaluated polynomial value
287316
*/
288317
template<typename T>
289318
T evaluate_power_series_via_bernstein( const T& x,
290-
const std::vector<T>& power_coeffs,
319+
const T* power_coeffs,
320+
size_t num_coefficients,
291321
const T& x_min,
292322
const T& x_max )
293323
{
294-
if( power_coeffs.empty() )
324+
if( num_coefficients == 0 )
295325
throw std::invalid_argument( "BersteinPolynomial::evaluate_power_series_via_bernstein: coefficients cannot be empty" );
296326

297327
// Convert to Bernstein representation
298-
const std::vector<T> bernstein_coeffs = power_series_to_bernstein( power_coeffs, x_min, x_max );
328+
const std::vector<T> bernstein_coeffs = power_series_to_bernstein( power_coeffs, num_coefficients, x_min, x_max );
299329

300330
// Transform x to [0, 1]
301331
const T x_normalized = (x - x_min) / (x_max - x_min);
302332

303333
// Evaluate Bernstein polynomial
304-
return evaluate( x_normalized, bernstein_coeffs );
334+
return evaluate( x_normalized, bernstein_coeffs.data(), bernstein_coeffs.size() );
305335
}
306336

307337

@@ -360,23 +390,47 @@ std::vector<T> fit_bernstein_lls( const std::vector<T>& x_values,
360390
// Normalize x to [0,1]
361391
const T x_norm = (x_values[i] - x_min) / x_range;
362392
const T inv_sigma = T(1) / uncertainties[i];
393+
const T one_minus_x = T(1) - x_norm;
363394

364395
// Fill row i of design matrix with weighted Bernstein basis functions
365-
for( unsigned int j = 0; j <= degree; ++j )
396+
// Use optimized power computation for common small degrees
397+
if( degree <= 6 )
366398
{
367-
// Compute B_j,degree(x_norm)
368-
const T binomial_coeff = binomial_coefficient<T>( degree, j );
399+
T x_powers[7], one_minus_x_powers[7];
369400

370-
T x_pow_j = T(1);
371-
for( unsigned int k = 0; k < j; ++k )
372-
x_pow_j *= x_norm;
401+
x_powers[0] = T(1);
402+
one_minus_x_powers[0] = T(1);
373403

374-
T one_minus_x_pow = T(1);
375-
const T one_minus_x = T(1) - x_norm;
376-
for( unsigned int k = 0; k < (degree - j); ++k )
377-
one_minus_x_pow *= one_minus_x;
404+
for( unsigned int k = 1; k <= degree; ++k )
405+
{
406+
x_powers[k] = x_powers[k-1] * x_norm;
407+
one_minus_x_powers[k] = one_minus_x_powers[k-1] * one_minus_x;
408+
}
378409

379-
A[i][j] = binomial_coeff * x_pow_j * one_minus_x_pow * inv_sigma;
410+
for( unsigned int j = 0; j <= degree; ++j )
411+
{
412+
const T binomial_coeff = binomial_coefficient<T>( degree, j );
413+
const T basis_value = binomial_coeff * x_powers[j] * one_minus_x_powers[degree-j];
414+
A[i][j] = basis_value * inv_sigma;
415+
}
416+
}
417+
else
418+
{
419+
// Fallback for higher degrees (rare case)
420+
for( unsigned int j = 0; j <= degree; ++j )
421+
{
422+
const T binomial_coeff = binomial_coefficient<T>( degree, j );
423+
424+
T x_pow_j = T(1);
425+
for( unsigned int k = 0; k < j; ++k )
426+
x_pow_j *= x_norm;
427+
428+
T one_minus_x_pow = T(1);
429+
for( unsigned int k = 0; k < (degree - j); ++k )
430+
one_minus_x_pow *= one_minus_x;
431+
432+
A[i][j] = binomial_coeff * x_pow_j * one_minus_x_pow * inv_sigma;
433+
}
380434
}
381435

382436
// Weighted y value
@@ -427,6 +481,44 @@ std::vector<T> fit_bernstein_lls( const std::vector<T>& x_values,
427481
return coeffs;
428482
}
429483

484+
485+
// Convenience overloads for backward compatibility with std::vector
486+
487+
/** Convenience overload for evaluate() that accepts std::vector */
488+
template<typename T>
489+
T evaluate( const T& x, const std::vector<T>& coefficients )
490+
{
491+
return evaluate( x, coefficients.data(), coefficients.size() );
492+
}
493+
494+
/** Convenience overload for power_series_to_bernstein() that accepts std::vector */
495+
template<typename T>
496+
std::vector<T> power_series_to_bernstein( const std::vector<T>& power_coeffs,
497+
const T& x_min,
498+
const T& x_max )
499+
{
500+
return power_series_to_bernstein( power_coeffs.data(), power_coeffs.size(), x_min, x_max );
501+
}
502+
503+
/** Convenience overload for bernstein_to_power_series() that accepts std::vector */
504+
template<typename T>
505+
std::vector<T> bernstein_to_power_series( const std::vector<T>& bernstein_coeffs,
506+
const T& x_min,
507+
const T& x_max )
508+
{
509+
return bernstein_to_power_series( bernstein_coeffs.data(), bernstein_coeffs.size(), x_min, x_max );
510+
}
511+
512+
/** Convenience overload for evaluate_power_series_via_bernstein() that accepts std::vector */
513+
template<typename T>
514+
T evaluate_power_series_via_bernstein( const T& x,
515+
const std::vector<T>& power_coeffs,
516+
const T& x_min,
517+
const T& x_max )
518+
{
519+
return evaluate_power_series_via_bernstein( x, power_coeffs.data(), power_coeffs.size(), x_min, x_max );
520+
}
521+
430522
} // namespace BersteinPolynomial
431523

432524
#endif // BersteinPolynomial_hpp

InterSpec/EnergyCal.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,31 @@ propogate_energy_cal_change( const std::shared_ptr<const SpecUtils::EnergyCalibr
206206
const std::shared_ptr<const SpecUtils::EnergyCalibration> &new_cal,
207207
const std::shared_ptr<const SpecUtils::EnergyCalibration> &other_cal );
208208

209+
210+
/** Internal function for fitting polynomial coefficients from channel-energy pairs.
211+
212+
@param channels_energies Vector of (channel, energy) pairs to fit to
213+
@param poly_terms Number of polynomial terms to fit
214+
@returns Vector of polynomial coefficients
215+
216+
Throws exception on error.
217+
*/
218+
std::vector<float> fit_for_poly_coefs( const std::vector<std::pair<double,double>> &channels_energies,
219+
const int poly_terms );
220+
221+
/** Internal function for fitting full range fraction coefficients from channel-energy pairs.
222+
223+
@param channels_energies Vector of (channel, energy) pairs to fit to
224+
@param nchannels Number of channels in the spectrum
225+
@param nterms Number of terms to fit (max 5, with 5th term being 1/(1+60*x))
226+
@returns Vector of full range fraction coefficients
227+
228+
Throws exception on error.
229+
*/
230+
std::vector<float> fit_for_fullrangefraction_coefs( const std::vector<std::pair<double,double>> &channels_energies,
231+
const size_t nchannels,
232+
const int nterms );
233+
209234
}//namespace EnergyCal
210235

211236
#endif //EnergyCal_h

0 commit comments

Comments
 (0)