@@ -2166,7 +2166,165 @@ bool write_CALp_file( std::ostream &output, const shared_ptr<const EnergyCalibra
21662166 }// if( cal->type() == FullRangeFraction )
21672167
21682168 output << " #END" << eol_char << eol_char;
2169-
2169+
21702170 return output.good ();
21712171}// void write_CALp_file(...)
2172+
2173+
2174+ std::vector<float > fit_poly_energy_cal_from_points ( const std::vector<std::pair<float ,float >> &channel_energy_pairs,
2175+ const size_t max_orders )
2176+ {
2177+ using namespace std ;
2178+
2179+ // Validate input
2180+ if ( channel_energy_pairs.empty () )
2181+ throw runtime_error ( " fit_poly_energy_cal_from_points: No data points provided" );
2182+
2183+ if ( max_orders == 0 )
2184+ throw runtime_error ( " fit_poly_energy_cal_from_points: max_orders must be at least 1" );
2185+
2186+ if ( max_orders > channel_energy_pairs.size () )
2187+ throw runtime_error ( " fit_poly_energy_cal_from_points: max_orders cannot exceed number of data points" );
2188+
2189+ // Validate that all input channel and energy values are finite
2190+ for ( size_t i = 0 ; i < channel_energy_pairs.size (); ++i )
2191+ {
2192+ const float ch = channel_energy_pairs[i].first ;
2193+ const float en = channel_energy_pairs[i].second ;
2194+
2195+ if ( isnan ( ch ) || isinf ( ch ) )
2196+ throw runtime_error ( " fit_poly_energy_cal_from_points: Channel value is NaN or Inf at index " + std::to_string (i) );
2197+
2198+ if ( isnan ( en ) || isinf ( en ) )
2199+ throw runtime_error ( " fit_poly_energy_cal_from_points: Energy value is NaN or Inf at index " + std::to_string (i) );
2200+ }
2201+
2202+ const size_t n = channel_energy_pairs.size ();
2203+ const int num_coefficients = static_cast <int >( max_orders );
2204+
2205+ vector<float > coeffs;
2206+
2207+ if ( num_coefficients == 1 )
2208+ {
2209+ // Single coefficient: fit only gain (no offset)
2210+ // E = 0 + gain * channel
2211+ // Solve: gain = sum(channel_i * energy_i) / sum(channel_i^2)
2212+
2213+ double sum_ch_e = 0.0 ;
2214+ double sum_ch2 = 0.0 ;
2215+
2216+ for ( const auto &pair : channel_energy_pairs )
2217+ {
2218+ const double ch = pair.first ;
2219+ const double en = pair.second ;
2220+ sum_ch_e += ch * en;
2221+ sum_ch2 += ch * ch;
2222+ }
2223+
2224+ if ( sum_ch2 < 1e-10 )
2225+ throw runtime_error ( " fit_poly_energy_cal_from_points: All channel values are zero or near-zero" );
2226+
2227+ const float gain = static_cast <float >( sum_ch_e / sum_ch2 );
2228+
2229+ if ( isnan ( gain ) || isinf ( gain ) )
2230+ throw runtime_error ( " fit_poly_energy_cal_from_points: Computed gain is NaN or Inf" );
2231+
2232+ // Return [offset=0, gain] to match polynomial format
2233+ coeffs.push_back ( 0 .0f ); // offset
2234+ coeffs.push_back ( gain ); // gain
2235+ }
2236+ else
2237+ {
2238+ // Multiple coefficients: use least squares fitting
2239+ // Solve normal equations for polynomial: E = c0 + c1*ch + c2*ch^2 + ...
2240+
2241+ // Build normal equations matrix: (X^T * X) * coefs = X^T * Y
2242+ // For unweighted least squares
2243+ vector<vector<double >> matrix ( num_coefficients, vector<double >( num_coefficients, 0.0 ) );
2244+ vector<double > rhs ( num_coefficients, 0.0 );
2245+
2246+ for ( size_t i = 0 ; i < n; ++i )
2247+ {
2248+ const double ch = channel_energy_pairs[i].first ;
2249+ const double en = channel_energy_pairs[i].second ;
2250+
2251+ // Build X^T * X and X^T * Y
2252+ for ( int row = 0 ; row < num_coefficients; ++row )
2253+ {
2254+ const double ch_pow_row = pow ( ch, row );
2255+
2256+ // Right hand side: X^T * Y
2257+ rhs[row] += ch_pow_row * en;
2258+
2259+ // Matrix: X^T * X
2260+ for ( int col = 0 ; col < num_coefficients; ++col )
2261+ {
2262+ const double ch_pow_col = pow ( ch, col );
2263+ matrix[row][col] += ch_pow_row * ch_pow_col;
2264+ }
2265+ }
2266+ }
2267+
2268+ // Solve using Gaussian elimination with partial pivoting
2269+ for ( int k = 0 ; k < num_coefficients; ++k )
2270+ {
2271+ // Find pivot
2272+ int pivot_row = k;
2273+ double max_val = fabs ( matrix[k][k] );
2274+ for ( int i = k + 1 ; i < num_coefficients; ++i )
2275+ {
2276+ if ( fabs ( matrix[i][k] ) > max_val )
2277+ {
2278+ max_val = fabs ( matrix[i][k] );
2279+ pivot_row = i;
2280+ }
2281+ }
2282+
2283+ if ( max_val < 1e-10 )
2284+ throw runtime_error ( " fit_poly_energy_cal_from_points: Singular matrix (determinant near zero)" );
2285+
2286+ // Swap rows if needed
2287+ if ( pivot_row != k )
2288+ {
2289+ swap ( matrix[k], matrix[pivot_row] );
2290+ swap ( rhs[k], rhs[pivot_row] );
2291+ }
2292+
2293+ // Eliminate column
2294+ for ( int i = k + 1 ; i < num_coefficients; ++i )
2295+ {
2296+ const double factor = matrix[i][k] / matrix[k][k];
2297+ for ( int j = k; j < num_coefficients; ++j )
2298+ matrix[i][j] -= factor * matrix[k][j];
2299+ rhs[i] -= factor * rhs[k];
2300+ }
2301+ }
2302+
2303+ // Back substitution
2304+ coeffs.resize ( num_coefficients );
2305+ for ( int i = num_coefficients - 1 ; i >= 0 ; --i )
2306+ {
2307+ double sum = rhs[i];
2308+ for ( int j = i + 1 ; j < num_coefficients; ++j )
2309+ sum -= matrix[i][j] * coeffs[j];
2310+ coeffs[i] = static_cast <float >( sum / matrix[i][i] );
2311+ }
2312+
2313+ // Validate coefficients
2314+ for ( size_t i = 0 ; i < coeffs.size (); ++i )
2315+ {
2316+ if ( isnan ( coeffs[i] ) || isinf ( coeffs[i] ) )
2317+ throw runtime_error ( " fit_poly_energy_cal_from_points: Computed coefficient is NaN or Inf at index " + std::to_string (i) );
2318+ }
2319+ }
2320+
2321+ // Final validation of all coefficients before returning
2322+ for ( size_t i = 0 ; i < coeffs.size (); ++i )
2323+ {
2324+ if ( isnan ( coeffs[i] ) || isinf ( coeffs[i] ) )
2325+ throw runtime_error ( " fit_poly_energy_cal_from_points: Final coefficient is NaN or Inf at index " + std::to_string (i) );
2326+ }
2327+
2328+ return coeffs;
2329+ }// fit_poly_energy_cal_from_points(...)
21722330}// namespace SpecUtils
0 commit comments