// SPDX-License-Identifier: MPL-2.0 #include "aare/Fit.hpp" #include "FitHelpers.hpp" #include "FitMinuit2.hpp" #include "LevenbergMarquardt.hpp" #include "VariableProjection.hpp" #include "aare/Models.hpp" #include "aare/utils/par.hpp" #include "aare/utils/task.hpp" #include #include #include #include #include #include namespace aare { // ============================================================================ // FitModel — method definitions // ============================================================================ template FitModel::FitModel(unsigned int strategy, unsigned int max_calls, double tolerance, bool compute_errors, Minimizer minimizer) : strategy_(strategy), max_calls_(max_calls), tolerance_(tolerance), compute_errors_(compute_errors), minimizer_(minimizer) { for (std::size_t i = 0; i < npar; ++i) { lower_[i] = Model::param_info[i].default_lo; upper_[i] = Model::param_info[i].default_hi; } } template void FitModel::check_index(unsigned int idx) const { if (idx >= npar) throw std::out_of_range("FitModel: parameter index " + std::to_string(idx) + " out of range"); } template unsigned int FitModel::checked_index(const std::string &name) const { for (std::size_t i = 0; i < npar; ++i) { if (name == Model::param_info[i].name) return static_cast(i); } throw std::runtime_error("FitModel: unknown parameter name '" + name + "'"); } template void FitModel::SetParLimits(unsigned int idx, double lo, double hi) { check_index(idx); if (!(lo < hi)) throw std::runtime_error( "FitModel: lower limit must be below upper limit for parameter '" + GetParName(idx) + "'"); lower_[idx] = lo; upper_[idx] = hi; } template void FitModel::FixParameter(unsigned int idx, double val) { SetParameter(idx, val); user_fixed_[idx] = true; } template void FitModel::ReleaseParameter(unsigned int idx) { check_index(idx); user_fixed_[idx] = false; } template void FitModel::ReleaseParameter(const std::string &name) { ReleaseParameter(checked_index(name)); } template void FitModel::SetParameter(unsigned int idx, double val) { check_index(idx); value_[idx] = val; user_start_[idx] = true; } template void FitModel::SetParameter(const std::string &name, double val) { SetParameter(checked_index(name), val); } template void FitModel::FixParameter(const std::string &name, double val) { FixParameter(checked_index(name), val); } template void FitModel::SetParLimits(const std::string &name, double lo, double hi) { SetParLimits(checked_index(name), lo, hi); } template std::string FitModel::GetParName(unsigned int idx) const { check_index(idx); return Model::param_info[idx].name; } template std::vector FitModel::GetParNames() const { std::vector names; names.reserve(npar); for (std::size_t i = 0; i < npar; ++i) names.emplace_back(Model::param_info[i].name); return names; } // ============================================================================ // fit_pixel / fit_3d // ============================================================================ namespace detail { struct NotSeparable {}; /** * @brief Fits pixels with the minimizer selected by the model. * * Keep one instance per thread: the buffers of the built-in minimizers are * reused between pixels so that a data cube is fitted without per-pixel * allocations. The Minuit2 minimizers create their state per pixel. */ template class PixelFitter { public: /** * @brief Fit one pixel. * * @param par_out Receives npar fitted values; zeros on failure. * @param err_out Receives npar errors when non-null; zeros on failure. * @param chi2 Receives the chi-squared at the minimum; 0 on failure. * @return true for a valid minimum. */ bool fit(const FitModel &model, NDView x, NDView y, NDView y_err, double *par_out, double *err_out, double &chi2) { const auto start = start_values(model, x, y); if (model.minimizer() == Minimizer::VarPro) { // A model without the separable structure, a pixel that does // not converge and a pixel whose linear solution violates a // limit fall back to the full solver. if constexpr (model::is_separable::value) { const auto res = vp_.fit(model, x, y, y_err, start, par_out, err_out); if (res.valid) { chi2 = res.chi2; return true; } } return fit_lm(model, x, y, y_err, start, par_out, err_out, chi2); } if (model.minimizer() == Minimizer::LevenbergMarquardt) return fit_lm(model, x, y, y_err, start, par_out, err_out, chi2); return fit_pixel_minuit2(model, x, y, y_err, start, par_out, err_out, chi2); } private: bool fit_lm(const FitModel &model, NDView x, NDView y, NDView y_err, const std::array &start, double *par_out, double *err_out, double &chi2) { const auto res = lm_.fit(model, x, y, y_err, start, par_out, err_out); chi2 = res.valid ? res.chi2 : 0.0; return res.valid; } LevenbergMarquardt lm_; std::conditional_t::value, VariableProjection, NotSeparable> vp_; }; } // namespace detail template NDArray fit_pixel(const FitModel &model, NDView x, NDView y, NDView y_err) { constexpr std::size_t npar = Model::npar; const bool want_errors = model.compute_errors(); const auto result_size = static_cast(want_errors ? (2 * npar + 1) : (npar + 1)); NDArray result({result_size}, 0.0); detail::PixelFitter fitter; double chi2 = 0.0; double *par = result.data(); double *err = want_errors ? result.data() + npar : nullptr; if (!fitter.fit(model, x, y, y_err, par, err, chi2)) return result; // all zeros result(result_size - 1) = chi2; return result; } template NDArray fit_pixel(const FitModel &model, NDView x, NDView y) { return fit_pixel(model, x, y, NDView{}); } template void fit_3d(const FitModel &model, NDView x, NDView y, NDView y_err, NDView par_out, NDView err_out, NDView chi2_out, int n_threads) { constexpr std::size_t npar = Model::npar; detail::check_fit_3d_shapes(x, y, y_err, par_out, err_out, chi2_out); const bool has_errors = (y_err.size() > 0); const bool want_par_errors = (err_out.size() > 0) && model.compute_errors(); auto process = [&](ssize_t first_row, ssize_t last_row) { detail::PixelFitter fitter; // buffers reused within the task std::array par{}; std::array err{}; for (ssize_t row = first_row; row < last_row; row++) { for (ssize_t col = 0; col < y.shape(1); col++) { NDView values(&y(row, col, 0), {y.shape(2)}); NDView errors = has_errors ? NDView(&y_err(row, col, 0), {y_err.shape(2)}) : NDView{}; double chi2 = 0.0; fitter.fit(model, x, values, errors, par.data(), want_par_errors ? err.data() : nullptr, chi2); for (std::size_t k = 0; k < npar; ++k) par_out(row, col, static_cast(k)) = par[k]; if (want_par_errors) { for (std::size_t k = 0; k < npar; ++k) err_out(row, col, static_cast(k)) = err[k]; } chi2_out(row, col) = chi2; } } }; auto tasks = split_task(0, static_cast(y.shape(0)), n_threads); RunInParallel(process, tasks); } // ============================================================================ // Explicit instantiations for all supported model types // ============================================================================ // NOLINTBEGIN #define AARE_INSTANTIATE_FIT(Model) \ template class FitModel; \ template NDArray fit_pixel( \ const FitModel &, NDView, NDView, \ NDView); \ template NDArray fit_pixel( \ const FitModel &, NDView, NDView); \ template void fit_3d(const FitModel &, NDView, \ NDView, NDView, \ NDView, NDView, \ NDView, int); AARE_INSTANTIATE_FIT(model::Gaussian) AARE_INSTANTIATE_FIT(model::GaussianErfcPlateau) AARE_INSTANTIATE_FIT(model::GaussianChargeSharing) AARE_INSTANTIATE_FIT(model::GaussianChargeSharingKb) AARE_INSTANTIATE_FIT(model::Pol1) AARE_INSTANTIATE_FIT(model::Pol2) AARE_INSTANTIATE_FIT(model::RisingScurve) AARE_INSTANTIATE_FIT(model::FallingScurve) #undef AARE_INSTANTIATE_FIT // NOLINTEND } // namespace aare