mirror of
https://github.com/slsdetectorgroup/aare.git
synced 2026-06-05 19:28:41 +02:00
Merge branch 'main' into feature/cuda_clusterfinder
This commit is contained in:
+36
-30
@@ -1,12 +1,12 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#pragma once
|
||||
#include <stdexcept>
|
||||
#include "aare/Models.hpp"
|
||||
#include "aare/NDView.hpp"
|
||||
#include <Minuit2/FCNGradientBase.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <Minuit2/FCNGradientBase.h>
|
||||
#include "aare/NDView.hpp"
|
||||
#include "aare/Models.hpp"
|
||||
|
||||
namespace aare {
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace func {
|
||||
*
|
||||
* By providing analytic gradients we avoid 2*npar extra function evaluations
|
||||
* per Minuit step that would otherwise be spent on finite differences.
|
||||
*
|
||||
*
|
||||
* @throws std::invalid_argument if par.size() != Model::npar.
|
||||
*
|
||||
* Invalid model parameters do not throw; they return a large penalty
|
||||
@@ -37,31 +37,32 @@ namespace func {
|
||||
*/
|
||||
template <class Model>
|
||||
class Chi2Model1DGrad : public ROOT::Minuit2::FCNGradientBase {
|
||||
public:
|
||||
Chi2Model1DGrad(NDView<double, 1> x,
|
||||
NDView<double, 1> y)
|
||||
public:
|
||||
Chi2Model1DGrad(NDView<double, 1> x, NDView<double, 1> y)
|
||||
: x_(x), y_(y), s_(), weighted_(false) {}
|
||||
|
||||
Chi2Model1DGrad(NDView<double, 1> x,
|
||||
NDView<double, 1> y,
|
||||
Chi2Model1DGrad(NDView<double, 1> x, NDView<double, 1> y,
|
||||
NDView<double, 1> y_err)
|
||||
: x_(x), y_(y), s_(y_err), weighted_(true) {}
|
||||
|
||||
~Chi2Model1DGrad() override = default;
|
||||
|
||||
double operator()(const std::vector<double>& par) const override {
|
||||
double operator()(const std::vector<double> &par) const override {
|
||||
if (par.size() != Model::npar) {
|
||||
throw std::invalid_argument("Chi2Model1DGrad: wrong parameter vector size.");
|
||||
throw std::invalid_argument(
|
||||
"Chi2Model1DGrad: wrong parameter vector size.");
|
||||
}
|
||||
|
||||
if (!Model::is_valid(par)) return 1e20;
|
||||
|
||||
if (!Model::is_valid(par))
|
||||
return 1e20;
|
||||
|
||||
double chi2 = 0.0;
|
||||
|
||||
if (weighted_) {
|
||||
for (ssize_t i = 0; i < x_.size(); ++i) {
|
||||
const double si = s_[i];
|
||||
if (si == 0.0) continue;
|
||||
if (si == 0.0)
|
||||
continue;
|
||||
|
||||
const double f_i = Model::eval(x_[i], par);
|
||||
const double r_i = y_[i] - f_i;
|
||||
@@ -78,13 +79,16 @@ public:
|
||||
return chi2;
|
||||
}
|
||||
|
||||
std::vector<double> Gradient(const std::vector<double>& par) const override {
|
||||
std::vector<double>
|
||||
Gradient(const std::vector<double> &par) const override {
|
||||
if (par.size() != Model::npar) {
|
||||
throw std::invalid_argument("Chi2Model1DGrad: wrong parameter vector size.");
|
||||
throw std::invalid_argument(
|
||||
"Chi2Model1DGrad: wrong parameter vector size.");
|
||||
}
|
||||
|
||||
|
||||
std::vector<double> grad(Model::npar, 0.0);
|
||||
if (!Model::is_valid(par)) return grad;
|
||||
if (!Model::is_valid(par))
|
||||
return grad;
|
||||
|
||||
std::array<double, Model::npar> df{};
|
||||
double f_i = 0.0;
|
||||
@@ -92,12 +96,13 @@ public:
|
||||
if (weighted_) {
|
||||
for (ssize_t i = 0; i < x_.size(); ++i) {
|
||||
const double si = s_[i];
|
||||
if (si == 0.0) continue;
|
||||
if (si == 0.0)
|
||||
continue;
|
||||
|
||||
Model::eval_and_grad(x_[i], par, f_i, df);
|
||||
|
||||
const double r_i = y_[i] - f_i;
|
||||
const double c = -2.0 * r_i / (si * si);
|
||||
const double c = -2.0 * r_i / (si * si);
|
||||
|
||||
for (std::size_t k = 0; k < Model::npar; ++k) {
|
||||
grad[k] += c * df[k];
|
||||
@@ -108,7 +113,7 @@ public:
|
||||
Model::eval_and_grad(x_[i], par, f_i, df);
|
||||
|
||||
const double r_i = y_[i] - f_i;
|
||||
const double c = -2.0 * r_i;
|
||||
const double c = -2.0 * r_i;
|
||||
|
||||
for (std::size_t k = 0; k < Model::npar; ++k) {
|
||||
grad[k] += c * df[k];
|
||||
@@ -119,10 +124,11 @@ public:
|
||||
return grad;
|
||||
}
|
||||
|
||||
/** @brief Error definition: 1.0 for chi-squared (delta_chi2 = 1 -> 1-sigma). */
|
||||
/** @brief Error definition: 1.0 for chi-squared (delta_chi2 = 1 ->
|
||||
* 1-sigma). */
|
||||
double Up() const override { return 1.0; }
|
||||
|
||||
private:
|
||||
private:
|
||||
NDView<double, 1> x_;
|
||||
NDView<double, 1> y_;
|
||||
NDView<double, 1> s_;
|
||||
@@ -131,12 +137,12 @@ private:
|
||||
|
||||
// ── Convenient aliases ──────────────────────────────────────────────
|
||||
|
||||
using Chi2Gaussian = Chi2Model1DGrad<aare::model::Gaussian>;
|
||||
using Chi2RisingScurve = Chi2Model1DGrad<aare::model::RisingScurve>;
|
||||
using Chi2Gaussian = Chi2Model1DGrad<aare::model::Gaussian>;
|
||||
using Chi2RisingScurve = Chi2Model1DGrad<aare::model::RisingScurve>;
|
||||
using Chi2FallingScurve = Chi2Model1DGrad<aare::model::FallingScurve>;
|
||||
using Chi2Pol1 = Chi2Model1DGrad<aare::model::Pol1>;
|
||||
using Chi2Pol2 = Chi2Model1DGrad<aare::model::Pol2>;
|
||||
using Chi2Pol1 = Chi2Model1DGrad<aare::model::Pol1>;
|
||||
using Chi2Pol2 = Chi2Model1DGrad<aare::model::Pol2>;
|
||||
|
||||
} // namespace aare::func
|
||||
} // namespace func
|
||||
|
||||
} // aare
|
||||
} // namespace aare
|
||||
@@ -17,7 +17,7 @@ template <class ItemType> class CircularFifo {
|
||||
aare::ProducerConsumerQueue<ItemType> filled_slots;
|
||||
|
||||
public:
|
||||
CircularFifo() : CircularFifo(100){};
|
||||
CircularFifo() : CircularFifo(100) {};
|
||||
CircularFifo(uint32_t size)
|
||||
: fifo_size(size), free_slots(size + 1), filled_slots(size + 1) {
|
||||
|
||||
|
||||
@@ -59,6 +59,27 @@ class ClusterVector<Cluster<T, ClusterSizeX, ClusterSizeY, CoordType>> {
|
||||
other.m_data.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create a copy of the clustervector by filtering clusters in the
|
||||
* ClusterVector using a boolean mask.
|
||||
* @param mask boolean 1d mask
|
||||
* @return ClusterVector containing only the clusters where the mask is
|
||||
* true
|
||||
*/
|
||||
ClusterVector operator()(NDView<bool, 1> mask) {
|
||||
if (static_cast<size_t>(mask.size()) != m_data.size()) {
|
||||
throw std::runtime_error(
|
||||
LOCATION + "Mask size does not match number of clusters");
|
||||
}
|
||||
ClusterVector result(capacity(), frame_number());
|
||||
for (size_t i = 0; i < m_data.size(); ++i) {
|
||||
if (mask(i)) {
|
||||
result.push_back(m_data[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Move assignment operator
|
||||
ClusterVector &operator=(ClusterVector &&other) noexcept {
|
||||
if (this != &other) {
|
||||
|
||||
+66
-66
@@ -5,17 +5,17 @@
|
||||
#include <fmt/core.h>
|
||||
#include <vector>
|
||||
|
||||
#include "aare/utils/par.hpp"
|
||||
#include "aare/utils/task.hpp"
|
||||
#include "aare/NDArray.hpp"
|
||||
#include "aare/Chi2.hpp"
|
||||
#include "aare/FitModel.hpp"
|
||||
#include "aare/NDArray.hpp"
|
||||
#include "aare/utils/par.hpp"
|
||||
#include "aare/utils/task.hpp"
|
||||
|
||||
#include "Minuit2/FunctionMinimum.h"
|
||||
#include "Minuit2/MnMigrad.h"
|
||||
#include "Minuit2/MnHesse.h"
|
||||
#include "Minuit2/MnUserParameters.h"
|
||||
#include "Minuit2/MnMigrad.h"
|
||||
#include "Minuit2/MnPrint.h"
|
||||
#include "Minuit2/MnUserParameters.h"
|
||||
|
||||
namespace aare {
|
||||
|
||||
@@ -34,7 +34,6 @@ NDArray<double, 1> scurve2(NDView<double, 1> x, NDView<double, 1> par);
|
||||
|
||||
} // namespace func
|
||||
|
||||
|
||||
static constexpr int DEFAULT_NUM_THREADS = 4;
|
||||
|
||||
/**
|
||||
@@ -80,7 +79,6 @@ void fit_gaus(NDView<double, 1> x, NDView<double, 3> y, NDView<double, 3> y_err,
|
||||
NDView<double, 3> par_out, NDView<double, 3> par_err_out,
|
||||
NDView<double, 2> chi2_out, int n_threads = DEFAULT_NUM_THREADS);
|
||||
|
||||
|
||||
NDArray<double, 1> fit_pol1(NDView<double, 1> x, NDView<double, 1> y);
|
||||
|
||||
NDArray<double, 3> fit_pol1(NDView<double, 1> x, NDView<double, 3> y,
|
||||
@@ -136,7 +134,8 @@ void fit_scurve2(NDView<double, 1> x, NDView<double, 3> y,
|
||||
* - Neither: both value and step size auto-filled from data.
|
||||
*
|
||||
* @tparam Model Model struct (Gaussian, RisingScurve, …).
|
||||
* @tparam FCN Chi2 functor type (Chi2Model1D or Chi2Model1DGrad instantiation).
|
||||
* @tparam FCN Chi2 functor type (Chi2Model1D or Chi2Model1DGrad
|
||||
* instantiation).
|
||||
*
|
||||
* @param model The FitModel configuration (read-only).
|
||||
* @param upar_local Thread-local clone of model.upar(). Modified in place.
|
||||
@@ -148,20 +147,19 @@ void fit_scurve2(NDView<double, 1> x, NDView<double, 3> y,
|
||||
* - compute_errors: [p0..pN, err0..errN, chi2] -> 2*npar + 1
|
||||
* - otherwise: [p0..pN, chi2] -> npar + 1
|
||||
*/
|
||||
template<typename Model, typename FCN>
|
||||
NDArray<double, 1> fit_pixel(const FitModel<Model>& model,
|
||||
ROOT::Minuit2::MnUserParameters& upar_local,
|
||||
NDView<double, 1> x,
|
||||
NDView<double, 1> y,
|
||||
NDView<double, 1> y_err) {
|
||||
template <typename Model, typename FCN>
|
||||
NDArray<double, 1> fit_pixel(const FitModel<Model> &model,
|
||||
ROOT::Minuit2::MnUserParameters &upar_local,
|
||||
NDView<double, 1> x, NDView<double, 1> y,
|
||||
NDView<double, 1> y_err) {
|
||||
|
||||
constexpr std::size_t npar = Model::npar;
|
||||
constexpr std::size_t npar = Model::npar;
|
||||
const bool want_errors = model.compute_errors();
|
||||
const ssize_t result_size = want_errors ? (2 * npar + 1) : (npar + 1);
|
||||
|
||||
// ──── automatic parameter estimation ─────────────
|
||||
auto start = Model::estimate_par(x,y);
|
||||
|
||||
auto start = Model::estimate_par(x, y);
|
||||
|
||||
// dead / degenerate pixel guard
|
||||
if (!Model::is_valid(std::vector<double>(start.begin(), start.end()))) {
|
||||
return NDArray<double, 1>({result_size}, 0.0);
|
||||
@@ -170,18 +168,18 @@ NDArray<double, 1> fit_pixel(const FitModel<Model>& model,
|
||||
// ──── data-range statistics for step sizes ─────────────
|
||||
double x_range, y_range, slope_scale;
|
||||
model::compute_ranges(x, y, x_range, y_range, slope_scale);
|
||||
|
||||
|
||||
std::array<double, npar> steps{};
|
||||
Model::compute_steps(start, x_range, y_range, slope_scale, steps);
|
||||
|
||||
// ── apply auto-estimates respecting user precedence ─────────────
|
||||
for(std::size_t i = 0; i < npar; ++i){
|
||||
for (std::size_t i = 0; i < npar; ++i) {
|
||||
// fixed: do not touch at all
|
||||
if(model.is_user_fixed(i)){
|
||||
if (model.is_user_fixed(i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!model.is_user_start(i)){
|
||||
if (!model.is_user_start(i)) {
|
||||
upar_local.SetValue(i, start[i]);
|
||||
}
|
||||
|
||||
@@ -193,7 +191,8 @@ NDArray<double, 1> fit_pixel(const FitModel<Model>& model,
|
||||
|
||||
// ──── run minimizer ────────
|
||||
ROOT::Minuit2::MnMigrad migrad(chi2, upar_local, model.strategy());
|
||||
ROOT::Minuit2::FunctionMinimum min = migrad(model.max_calls(), model.tolerance());
|
||||
ROOT::Minuit2::FunctionMinimum min =
|
||||
migrad(model.max_calls(), model.tolerance());
|
||||
|
||||
if (!min.IsValid())
|
||||
return NDArray<double, 1>({result_size}, 0.0);
|
||||
@@ -202,20 +201,20 @@ NDArray<double, 1> fit_pixel(const FitModel<Model>& model,
|
||||
if (want_errors) {
|
||||
ROOT::Minuit2::MnHesse hesse;
|
||||
hesse(chi2, min);
|
||||
|
||||
const auto& values = min.UserState().Params();
|
||||
const auto& errors = min.UserState().Errors();
|
||||
|
||||
|
||||
const auto &values = min.UserState().Params();
|
||||
const auto &errors = min.UserState().Errors();
|
||||
|
||||
NDArray<double, 1> result({result_size});
|
||||
for (std::size_t k = 0; k < npar; ++k) {
|
||||
result[k] = values[k];
|
||||
result[k] = values[k];
|
||||
result[npar + k] = errors[k];
|
||||
}
|
||||
result[2 * npar] = min.Fval();
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto& values = min.UserState().Params();
|
||||
|
||||
const auto &values = min.UserState().Params();
|
||||
NDArray<double, 1> result({result_size});
|
||||
for (std::size_t k = 0; k < npar; ++k)
|
||||
result[k] = values[k];
|
||||
@@ -225,21 +224,16 @@ NDArray<double, 1> fit_pixel(const FitModel<Model>& model,
|
||||
|
||||
// ── self-contained for 1D / standalone use ─────────
|
||||
template <typename Model, typename FCN>
|
||||
NDArray<double, 1> fit_pixel(const FitModel<Model>& model,
|
||||
NDView<double, 1> x,
|
||||
NDView<double, 1> y,
|
||||
NDView<double, 1> y_err)
|
||||
{
|
||||
NDArray<double, 1> fit_pixel(const FitModel<Model> &model, NDView<double, 1> x,
|
||||
NDView<double, 1> y, NDView<double, 1> y_err) {
|
||||
auto upar_local = model.upar();
|
||||
return fit_pixel<Model, FCN>(model, upar_local, x, y, y_err);
|
||||
}
|
||||
|
||||
// Overload: uncertainties not provided
|
||||
template <typename Model, typename FCN>
|
||||
NDArray<double, 1> fit_pixel(const FitModel<Model>& model,
|
||||
NDView<double, 1> x,
|
||||
NDView<double, 1> y)
|
||||
{
|
||||
NDArray<double, 1> fit_pixel(const FitModel<Model> &model, NDView<double, 1> x,
|
||||
NDView<double, 1> y) {
|
||||
auto upar_local = model.upar();
|
||||
return fit_pixel<Model, FCN>(model, upar_local, x, y, NDView<double, 1>{});
|
||||
}
|
||||
@@ -257,68 +251,74 @@ NDArray<double, 1> fit_pixel(const FitModel<Model>& model,
|
||||
* @param model Fit configuration shared by all pixels.
|
||||
* @param x Scan points, shape `(n_scan)`.
|
||||
* @param y Measured values, shape `(rows, cols, n_scan)`.
|
||||
* @param y_err Uncertainties, same shape as y, or empty for unweighted fits.
|
||||
* @param y_err Uncertainties, same shape as y, or empty for unweighted
|
||||
* fits.
|
||||
* @param par_out Output parameters, shape `(rows, cols, npar)`.
|
||||
* @param err_out Output parameter errors, shape `(rows, cols, npar)`, if used.
|
||||
* @param chi2_out Output chi-squared / objective values, shape `(rows, cols)`.
|
||||
* @param err_out Output parameter errors, shape `(rows, cols, npar)`, if
|
||||
* used.
|
||||
* @param chi2_out Output chi-squared / objective values, shape `(rows,
|
||||
* cols)`.
|
||||
* @param n_threads Number of threads used to split rows.
|
||||
*
|
||||
*/
|
||||
template <typename Model, typename FCN>
|
||||
void fit_3d(const FitModel<Model>& model,
|
||||
NDView<double, 1> x, // (n_scan)
|
||||
NDView<double, 3> y, // (rows, cols, n_scan)
|
||||
NDView<double, 3> y_err, // (rows, cols, n_scan) or empty for unweighted fit
|
||||
NDView<double, 3> par_out,
|
||||
NDView<double, 3> err_out,
|
||||
NDView<double, 2> chi2_out,
|
||||
int n_threads)
|
||||
{
|
||||
void fit_3d(
|
||||
const FitModel<Model> &model, NDView<double, 1> x, // (n_scan)
|
||||
NDView<double, 3> y, // (rows, cols, n_scan)
|
||||
NDView<double, 3> y_err, // (rows, cols, n_scan) or empty for unweighted fit
|
||||
NDView<double, 3> par_out, NDView<double, 3> err_out,
|
||||
NDView<double, 2> chi2_out, int n_threads) {
|
||||
const std::size_t npar = Model::npar;
|
||||
|
||||
// ──── checks ───────
|
||||
if (x.size() != y.shape(2))
|
||||
throw std::runtime_error("fit_3d: x.size() must match y.shape(2).");
|
||||
|
||||
if (par_out.shape(0) != y.shape(0) || par_out.shape(1) != y.shape(1) || par_out.shape(2) != npar)
|
||||
if (par_out.shape(0) != y.shape(0) || par_out.shape(1) != y.shape(1) ||
|
||||
par_out.shape(2) != npar)
|
||||
throw std::runtime_error("par_out must have shape [rows, cols, npar].");
|
||||
|
||||
if (chi2_out.shape(0) != y.shape(0) || chi2_out.shape(1) != y.shape(1))
|
||||
throw std::runtime_error("chi2_out must have shape [rows, cols].");
|
||||
|
||||
|
||||
const bool has_errors = (y_err.size() > 0);
|
||||
const bool want_par_errors = (err_out.size() > 0) && model.compute_errors();
|
||||
|
||||
if (has_errors) {
|
||||
if (y.shape(0) != y_err.shape(0) || y.shape(1) != y_err.shape(1) || y.shape(2) != y_err.shape(2))
|
||||
throw std::runtime_error("fit_3d: y and y_err must have identical shape.");
|
||||
if (y.shape(0) != y_err.shape(0) || y.shape(1) != y_err.shape(1) ||
|
||||
y.shape(2) != y_err.shape(2))
|
||||
throw std::runtime_error(
|
||||
"fit_3d: y and y_err must have identical shape.");
|
||||
|
||||
if (err_out.shape(0) != y.shape(0) || err_out.shape(1) != y.shape(1) || err_out.shape(2) != npar)
|
||||
throw std::runtime_error("err_out must have shape [rows, cols, npar].");
|
||||
if (err_out.shape(0) != y.shape(0) || err_out.shape(1) != y.shape(1) ||
|
||||
err_out.shape(2) != npar)
|
||||
throw std::runtime_error(
|
||||
"err_out must have shape [rows, cols, npar].");
|
||||
}
|
||||
|
||||
// ──── parallel dispatch ───────
|
||||
auto process = [&](ssize_t first_row, ssize_t last_row) {
|
||||
|
||||
// one clone per thread
|
||||
auto upar_local = model.upar();
|
||||
// one clone per thread
|
||||
auto upar_local = model.upar();
|
||||
|
||||
for (ssize_t row = first_row; row < last_row; row++) {
|
||||
for (ssize_t col = 0; col < y.shape(1); col++) {
|
||||
|
||||
NDView<double, 1> values(&y(row, col, 0), {y.shape(2)});
|
||||
NDView<double, 1> errors = has_errors
|
||||
? NDView<double, 1>(&y_err(row, col, 0), {y_err.shape(2)})
|
||||
: NDView<double, 1>{};
|
||||
NDView<double, 1> errors =
|
||||
has_errors ? NDView<double, 1>(&y_err(row, col, 0),
|
||||
{y_err.shape(2)})
|
||||
: NDView<double, 1>{};
|
||||
|
||||
auto res = fit_pixel<Model, FCN>(model, upar_local, x, values, errors);
|
||||
|
||||
for(std::size_t k = 0; k < npar; ++k) {
|
||||
auto res =
|
||||
fit_pixel<Model, FCN>(model, upar_local, x, values, errors);
|
||||
|
||||
for (std::size_t k = 0; k < npar; ++k) {
|
||||
par_out(row, col, k) = res(k);
|
||||
}
|
||||
|
||||
if (want_par_errors) {
|
||||
for(std::size_t k = 0; k < npar; ++k){
|
||||
for (std::size_t k = 0; k < npar; ++k) {
|
||||
err_out(row, col, k) = res(npar + k);
|
||||
}
|
||||
chi2_out(row, col) = res(2 * npar);
|
||||
|
||||
+72
-39
@@ -1,17 +1,15 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#pragma once
|
||||
|
||||
#include <type_traits>
|
||||
#include "aare/Models.hpp"
|
||||
#include <type_traits>
|
||||
|
||||
#include "Minuit2/MnUserParameters.h"
|
||||
#include "Minuit2/MnStrategy.h"
|
||||
#include "Minuit2/MnUserParameters.h"
|
||||
|
||||
namespace aare {
|
||||
|
||||
|
||||
template <typename Model>
|
||||
class FitModel {
|
||||
template <typename Model> class FitModel {
|
||||
ROOT::Minuit2::MnUserParameters upar_;
|
||||
ROOT::Minuit2::MnStrategy strategy_;
|
||||
unsigned int max_calls_;
|
||||
@@ -21,45 +19,51 @@ class FitModel {
|
||||
std::array<bool, Model::npar> user_fixed_{};
|
||||
std::array<bool, Model::npar> user_start_{};
|
||||
|
||||
public:
|
||||
/** @brief Safely resolve a parameter name to its index. */
|
||||
unsigned int checked_index(const std::string &name) const {
|
||||
for (std::size_t i = 0; i < npar; ++i) {
|
||||
if (upar_.Name(i) == name)
|
||||
return static_cast<unsigned int>(i);
|
||||
}
|
||||
throw std::runtime_error("FitModel: unknown parameter name '" + name +
|
||||
"'");
|
||||
}
|
||||
|
||||
public:
|
||||
static constexpr std::size_t npar = Model::npar;
|
||||
|
||||
/**
|
||||
* @brief Construct a fit model with sensible defaults.
|
||||
*
|
||||
* @param strategy Minuit2 strategy level (0 = fast/gradient, 1 = default).
|
||||
* @param strategy Minuit2 strategy level (0 = fast/gradient, 1 =
|
||||
* default).
|
||||
* @param max_calls Maximum FCN calls per pixel minimisation.
|
||||
* @param tolerance Minuit2 EDM tolerance.
|
||||
* @param compute_errors If true, run MnHesse after minimisation.
|
||||
*/
|
||||
FitModel(unsigned int strategy = 0,
|
||||
unsigned int max_calls = 100,
|
||||
double tolerance = 0.5,
|
||||
bool compute_errors = false)
|
||||
: strategy_(strategy),
|
||||
max_calls_(max_calls),
|
||||
tolerance_(tolerance),
|
||||
compute_errors_(compute_errors)
|
||||
{
|
||||
for(std::size_t i = 0; i < npar; ++i){
|
||||
FitModel(unsigned int strategy = 0, unsigned int max_calls = 100,
|
||||
double tolerance = 0.5, bool compute_errors = false)
|
||||
: strategy_(strategy), max_calls_(max_calls), tolerance_(tolerance),
|
||||
compute_errors_(compute_errors) {
|
||||
for (std::size_t i = 0; i < npar; ++i) {
|
||||
const auto pi = Model::param_info[i];
|
||||
const bool has_lo = std::isfinite(pi.default_lo);
|
||||
const bool has_hi = std::isfinite(pi.default_hi);
|
||||
|
||||
// Add parameters and valid bounds
|
||||
if (has_lo && has_hi){
|
||||
if (has_lo && has_hi) {
|
||||
upar_.Add(pi.name, 0.0, 1.0, pi.default_lo, pi.default_hi);
|
||||
} else if (has_lo) {
|
||||
upar_.Add(pi.name, 0.0, 1.0, pi.default_lo, 1e6);
|
||||
} else {
|
||||
upar_.Add(pi.name, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Set lower and upper bounds for parameter idx.*/
|
||||
void SetParLimits(unsigned int idx, double lo, double hi) {
|
||||
upar_.SetLimits(idx, lo, hi);
|
||||
void SetParLimits(unsigned int idx, double lo, double hi) {
|
||||
upar_.SetLimits(idx, lo, hi);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,38 +71,67 @@ public:
|
||||
*
|
||||
* Excluded from minimisation. Automatic estimates will not touch it.
|
||||
*/
|
||||
void FixParameter(unsigned int idx, double val) {
|
||||
upar_.SetValue(idx, val);
|
||||
void FixParameter(unsigned int idx, double val) {
|
||||
SetParameter(idx, val);
|
||||
upar_.Fix(idx);
|
||||
user_start_[idx] = true;
|
||||
user_fixed_[idx] = true;
|
||||
}
|
||||
|
||||
/** @brief Release a previously fixed parameter, re-enabling auto estimates. */
|
||||
|
||||
/** @brief Release a previously fixed parameter, re-enabling auto estimates.
|
||||
*/
|
||||
void ReleaseParameter(unsigned int idx) {
|
||||
upar_.Release(idx);
|
||||
user_fixed_[idx] = false;
|
||||
}
|
||||
|
||||
void ReleaseParameter(const std::string &name) {
|
||||
ReleaseParameter(checked_index(name));
|
||||
}
|
||||
|
||||
/** @brief Set an explicit starting value for parameter idx.*/
|
||||
void SetParameter(unsigned int idx, double val) {
|
||||
upar_.SetValue(idx, val);
|
||||
user_start_[idx] = true;
|
||||
}
|
||||
|
||||
void SetMaxCalls(unsigned int n) { max_calls_ = n; }
|
||||
void SetTolerance(double t) { tolerance_ = t; }
|
||||
void SetComputeErrors(bool b) { compute_errors_ = b; }
|
||||
|
||||
void SetParameter(const std::string &name, double val) {
|
||||
// go through index to maintain user_start_ bookkeeping
|
||||
SetParameter(checked_index(name), val);
|
||||
}
|
||||
|
||||
void FixParameter(const std::string &name, double val) {
|
||||
// go through index to maintain user_fixed_ bookkeeping
|
||||
FixParameter(checked_index(name), val);
|
||||
}
|
||||
|
||||
void SetParLimits(const std::string &name, double lo, double hi) {
|
||||
SetParLimits(checked_index(name), lo, hi);
|
||||
}
|
||||
|
||||
std::string GetParName(unsigned int idx) const {
|
||||
return upar_.GetName(idx);
|
||||
}
|
||||
|
||||
std::vector<std::string> GetParNames() const {
|
||||
std::vector<std::string> names;
|
||||
for (std::size_t i = 0; i < npar; ++i)
|
||||
names.push_back(GetParName(i));
|
||||
return names;
|
||||
}
|
||||
static constexpr std::size_t GetNpar() noexcept { return npar; }
|
||||
|
||||
void SetMaxCalls(unsigned int n) { max_calls_ = n; }
|
||||
void SetTolerance(double t) { tolerance_ = t; }
|
||||
void SetComputeErrors(bool b) { compute_errors_ = b; }
|
||||
|
||||
// accessors
|
||||
const ROOT::Minuit2::MnUserParameters& upar() const { return upar_; }
|
||||
const ROOT::Minuit2::MnStrategy& strategy() const { return strategy_; }
|
||||
unsigned int max_calls() const { return max_calls_; }
|
||||
double tolerance() const { return tolerance_; }
|
||||
bool compute_errors() const { return compute_errors_; }
|
||||
bool is_user_fixed(unsigned int idx) const { return user_fixed_[idx]; }
|
||||
bool is_user_start(unsigned int idx) const { return user_start_[idx]; }
|
||||
const ROOT::Minuit2::MnUserParameters &upar() const { return upar_; }
|
||||
const ROOT::Minuit2::MnStrategy &strategy() const { return strategy_; }
|
||||
unsigned int max_calls() const { return max_calls_; }
|
||||
double tolerance() const { return tolerance_; }
|
||||
bool compute_errors() const { return compute_errors_; }
|
||||
bool is_user_fixed(unsigned int idx) const { return user_fixed_[idx]; }
|
||||
bool is_user_start(unsigned int idx) const { return user_start_[idx]; }
|
||||
};
|
||||
|
||||
|
||||
} // namespace aare
|
||||
} // namespace aare
|
||||
|
||||
@@ -171,11 +171,14 @@ Coordinate2D Interpolator::transform_eta_values(const Eta2<T> &eta) const {
|
||||
|
||||
if (static_cast<ssize_t>(ix) >= m_etabinsx.size() - 1 ||
|
||||
static_cast<ssize_t>(iy) >= m_etabinsy.size() - 1 ||
|
||||
static_cast<ssize_t>(ie) >= m_energy_bins.size() - 1)
|
||||
throw std::runtime_error(
|
||||
fmt::format("Eta values out of bounds of eta distribution: eta.x = "
|
||||
"{:.4f}, eta.y = {:.4f}, energy = {:.4f}",
|
||||
eta.x, eta.y, eta.sum));
|
||||
static_cast<ssize_t>(ie) >= m_energy_bins.size() - 1) {
|
||||
throw std::runtime_error(fmt::format(
|
||||
"Eta values (eta.x = {:4f}, eta.y = {:4f}, energy = {}) out of "
|
||||
"bounds of eta distribution with largest values: (eta.x = {:4f}, "
|
||||
"eta.y = {:4f}, energy = {:4f})",
|
||||
eta.x, eta.y, eta.sum, *(m_etabinsx.end() - 1),
|
||||
*(m_etabinsy.end() - 1), *(m_energy_bins.end() - 1)));
|
||||
}
|
||||
|
||||
// TODO: bilinear interpolation only works if all bins have a size > 1 -
|
||||
// otherwise bilinear interpolation with zero values which skew the
|
||||
|
||||
+194
-178
@@ -1,18 +1,40 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#pragma once
|
||||
|
||||
#include "aare/NDView.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <limits>
|
||||
#include <algorithm>
|
||||
#include "aare/NDView.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace aare::model {
|
||||
|
||||
inline constexpr double inv_sqrt2 = 0.70710678118654752440;
|
||||
inline constexpr double inv_sqrt2 = 0.70710678118654752440;
|
||||
inline constexpr double inv_sqrt_2pi = 0.39894228040143267794;
|
||||
|
||||
inline double fast_erf(double x) {
|
||||
// Abramowitz–Stegun Handbook of Mathematical Functions
|
||||
// erf approximation with max error ~1.5e-7, faster than std::erf.
|
||||
|
||||
const double a1 = 0.254829592;
|
||||
const double a2 = -0.284496736;
|
||||
const double a3 = 1.421413741;
|
||||
const double a4 = -1.453152027;
|
||||
const double a5 = 1.061405429;
|
||||
const double p = 0.3275911;
|
||||
|
||||
const int sign = x < 0 ? -1 : 1;
|
||||
x = std::abs(x);
|
||||
|
||||
// 7.1.26
|
||||
const double t = 1.0 / (1.0 + p * x);
|
||||
const double y =
|
||||
1.0 -
|
||||
(((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t) * std::exp(-x * x);
|
||||
|
||||
return sign * y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Per-parameter metadata: name and optional default bounds.
|
||||
@@ -21,11 +43,11 @@ inline constexpr double inv_sqrt_2pi = 0.39894228040143267794;
|
||||
* Unbounded directions use ±no_bound as sentinels.
|
||||
*/
|
||||
struct ParamInfo {
|
||||
const char* name; // name of parameter
|
||||
double default_lo; // lower bound value
|
||||
double default_hi; // upper bound value
|
||||
const char *name; // name of parameter
|
||||
double default_lo; // lower bound value
|
||||
double default_hi; // upper bound value
|
||||
};
|
||||
|
||||
|
||||
inline constexpr double no_bound = std::numeric_limits<double>::infinity();
|
||||
|
||||
/**
|
||||
@@ -33,18 +55,15 @@ inline constexpr double no_bound = std::numeric_limits<double>::infinity();
|
||||
*
|
||||
* Model-independent, called once per pixel.
|
||||
*/
|
||||
inline void compute_ranges(NDView<double, 1> x,
|
||||
NDView<double, 1> y,
|
||||
double& x_range,
|
||||
double& y_range,
|
||||
double& slope_scale)
|
||||
{
|
||||
inline void compute_ranges(NDView<double, 1> x, NDView<double, 1> y,
|
||||
double &x_range, double &y_range,
|
||||
double &slope_scale) {
|
||||
const auto [x_min, x_max] = std::minmax_element(x.begin(), x.end());
|
||||
const auto [y_min, y_max] = std::minmax_element(y.begin(), y.end());
|
||||
|
||||
x_range = std::max(*x_max - *x_min, 1e-9);
|
||||
y_range = std::max(*y_max - *y_min, 1e-9);
|
||||
|
||||
|
||||
slope_scale = std::max(y_range / x_range, 1e-9);
|
||||
}
|
||||
|
||||
@@ -70,50 +89,45 @@ struct Pol1 {
|
||||
static constexpr std::size_t npar = 2;
|
||||
|
||||
static constexpr std::array<ParamInfo, npar> param_info = {{
|
||||
{"p0", -no_bound, no_bound},
|
||||
{"p1", -no_bound, no_bound},
|
||||
{"p0", -no_bound, no_bound},
|
||||
{"p1", -no_bound, no_bound},
|
||||
}};
|
||||
|
||||
static double eval(double x, const std::vector<double>& par) {
|
||||
static double eval(double x, const std::vector<double> &par) {
|
||||
return par[0] + par[1] * x;
|
||||
}
|
||||
|
||||
static void eval_and_grad(double x,
|
||||
const std::vector<double>& par,
|
||||
double& f,
|
||||
std::array<double, npar>& g)
|
||||
{
|
||||
static void eval_and_grad(double x, const std::vector<double> &par,
|
||||
double &f, std::array<double, npar> &g) {
|
||||
f = par[0] + par[1] * x;
|
||||
|
||||
g[0] = 1.0 ; // df/dp0
|
||||
g[1] = x; // df/dp1
|
||||
g[0] = 1.0; // df/dp0
|
||||
g[1] = x; // df/dp1
|
||||
}
|
||||
|
||||
static bool is_valid([[maybe_unused]] const std::vector<double>& par) {
|
||||
return true; // always valid
|
||||
static bool is_valid([[maybe_unused]] const std::vector<double> &par) {
|
||||
return true; // always valid
|
||||
}
|
||||
|
||||
/** @brief Estimate from endpoints: slope = dy/dx, intercept from first point. */
|
||||
/** @brief Estimate from endpoints: slope = dy/dx, intercept from first
|
||||
* point. */
|
||||
static std::array<double, npar> estimate_par(NDView<double, 1> x,
|
||||
NDView<double, 1> y)
|
||||
{
|
||||
const double dx = x[x.size()-1] - x[0];
|
||||
const double dy = y[y.size()-1] - y[0];
|
||||
const double slope = (std::abs(dx) > 1e-12) ? dy/dx : 0.0;
|
||||
NDView<double, 1> y) {
|
||||
const double dx = x[x.size() - 1] - x[0];
|
||||
const double dy = y[y.size() - 1] - y[0];
|
||||
const double slope = (std::abs(dx) > 1e-12) ? dy / dx : 0.0;
|
||||
const double intercept = y[0] - slope * x[0];
|
||||
|
||||
return {intercept, slope};
|
||||
}
|
||||
|
||||
|
||||
static void compute_steps(const std::array<double, npar>& start,
|
||||
[[maybe_unused]] double x_range, double y_range, double slope_scale,
|
||||
std::array<double, npar>& steps)
|
||||
{
|
||||
static void compute_steps(const std::array<double, npar> &start,
|
||||
[[maybe_unused]] double x_range, double y_range,
|
||||
double slope_scale,
|
||||
std::array<double, npar> &steps) {
|
||||
steps[0] = std::max(0.1 * std::abs(start[0]), 0.1 * y_range);
|
||||
steps[1] = 0.1 * slope_scale;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// _____________________________________________________________________
|
||||
@@ -124,7 +138,7 @@ struct Pol1 {
|
||||
/**
|
||||
* @brief Polynomial fuction of degree 2
|
||||
*
|
||||
* f(x) = p0 + p1 * x + p2 * x * x
|
||||
* f(x) = p0 + p1 * x + p2 * x * x
|
||||
*
|
||||
* Parameters:
|
||||
* par[0] = p0 (constant term)
|
||||
@@ -140,29 +154,26 @@ struct Pol2 {
|
||||
static constexpr std::size_t npar = 3;
|
||||
|
||||
static constexpr std::array<ParamInfo, npar> param_info = {{
|
||||
{"p0", -no_bound, no_bound},
|
||||
{"p1", -no_bound, no_bound},
|
||||
{"p2", -no_bound, no_bound},
|
||||
{"p0", -no_bound, no_bound},
|
||||
{"p1", -no_bound, no_bound},
|
||||
{"p2", -no_bound, no_bound},
|
||||
}};
|
||||
|
||||
static double eval(double x, const std::vector<double>& par) {
|
||||
static double eval(double x, const std::vector<double> &par) {
|
||||
return par[0] + par[1] * x + par[2] * x * x;
|
||||
}
|
||||
|
||||
static void eval_and_grad(double x,
|
||||
const std::vector<double>& par,
|
||||
double& f,
|
||||
std::array<double, npar>& g)
|
||||
{
|
||||
static void eval_and_grad(double x, const std::vector<double> &par,
|
||||
double &f, std::array<double, npar> &g) {
|
||||
f = par[0] + par[1] * x + par[2] * x * x;
|
||||
|
||||
g[0] = 1.0 ; // df/dp0
|
||||
g[1] = x; // df/dp1
|
||||
g[2] = x * x; // df/dp2
|
||||
g[0] = 1.0; // df/dp0
|
||||
g[1] = x; // df/dp1
|
||||
g[2] = x * x; // df/dp2
|
||||
}
|
||||
|
||||
static bool is_valid([[maybe_unused]]const std::vector<double>& par) {
|
||||
return true; // always valid
|
||||
static bool is_valid([[maybe_unused]] const std::vector<double> &par) {
|
||||
return true; // always valid
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,23 +195,22 @@ struct Pol2 {
|
||||
* when curvature is negligible i.e. slope_e ≈ slope_s -> p2 ≈ 0.
|
||||
*/
|
||||
static std::array<double, npar> estimate_par(NDView<double, 1> x,
|
||||
NDView<double, 1> y)
|
||||
{
|
||||
NDView<double, 1> y) {
|
||||
const ssize_t n = y.size();
|
||||
const ssize_t tail = std::max<ssize_t>(n / 10, 2);
|
||||
|
||||
// start: slope from first 10%
|
||||
const double x_s = (x[0] + x[tail-1]) * 0.5;
|
||||
const double slope_s = (y[tail-1] - y[0]) / (x[tail-1] - x[0]);
|
||||
const double x_s = (x[0] + x[tail - 1]) * 0.5;
|
||||
const double slope_s = (y[tail - 1] - y[0]) / (x[tail - 1] - x[0]);
|
||||
|
||||
// end: slope from last 10%
|
||||
const double x_e = (x[n-tail] + x[n-1]) * 0.5;
|
||||
const double slope_e = (y[n-1] - y[n-tail]) / (x[n-1] - x[n-tail]);
|
||||
const double x_e = (x[n - tail] + x[n - 1]) * 0.5;
|
||||
const double slope_e =
|
||||
(y[n - 1] - y[n - tail]) / (x[n - 1] - x[n - tail]);
|
||||
|
||||
const double dx = x_e - x_s;
|
||||
const double p2 = (std::abs(dx) > 1e-12)
|
||||
? (slope_e - slope_s) / (2.0 * dx)
|
||||
: 0.0;
|
||||
const double p2 =
|
||||
(std::abs(dx) > 1e-12) ? (slope_e - slope_s) / (2.0 * dx) : 0.0;
|
||||
|
||||
// p1 from slope_s = p1 + 2*p2*x_s
|
||||
const double p1 = slope_s - 2.0 * p2 * x_s;
|
||||
@@ -211,11 +221,10 @@ struct Pol2 {
|
||||
return {p0, p1, p2};
|
||||
}
|
||||
|
||||
|
||||
static void compute_steps(const std::array<double, npar>& start,
|
||||
double x_range, double y_range, double slope_scale,
|
||||
std::array<double, npar>& steps)
|
||||
{
|
||||
static void compute_steps(const std::array<double, npar> &start,
|
||||
double x_range, double y_range,
|
||||
double slope_scale,
|
||||
std::array<double, npar> &steps) {
|
||||
steps[0] = std::max(0.1 * std::abs(start[0]), 0.1 * y_range);
|
||||
steps[1] = 0.1 * slope_scale;
|
||||
steps[2] = 0.1 * slope_scale / std::max(x_range, 1e-12);
|
||||
@@ -246,43 +255,41 @@ struct Gaussian {
|
||||
static constexpr std::size_t npar = 3;
|
||||
|
||||
static constexpr std::array<ParamInfo, npar> param_info = {{
|
||||
{"A", -no_bound, no_bound},
|
||||
{"mu", -no_bound, no_bound},
|
||||
{"sig", 1e-12, no_bound},
|
||||
{"A", -no_bound, no_bound},
|
||||
{"mu", -no_bound, no_bound},
|
||||
{"sigma", 1e-12, no_bound},
|
||||
}};
|
||||
|
||||
static double eval(double x, const std::vector<double>& par) {
|
||||
const double A = par[0];
|
||||
const double mu = par[1];
|
||||
static double eval(double x, const std::vector<double> &par) {
|
||||
const double A = par[0];
|
||||
const double mu = par[1];
|
||||
const double sig = par[2];
|
||||
|
||||
const double dx = x - mu;
|
||||
const double dx = x - mu;
|
||||
const double inv_2sig2 = 1.0 / (2.0 * sig * sig);
|
||||
return A * std::exp(-dx * dx * inv_2sig2);
|
||||
}
|
||||
|
||||
static void eval_and_grad(double x,
|
||||
const std::vector<double>& par,
|
||||
double& f,
|
||||
std::array<double, npar>& g)
|
||||
{
|
||||
const double A = par[0];
|
||||
const double mu = par[1];
|
||||
static void eval_and_grad(double x, const std::vector<double> &par,
|
||||
double &f, std::array<double, npar> &g) {
|
||||
const double A = par[0];
|
||||
const double mu = par[1];
|
||||
const double sig = par[2];
|
||||
|
||||
const double dx = x - mu;
|
||||
const double dx = x - mu;
|
||||
const double inv_2sig2 = 1.0 / (2.0 * sig * sig);
|
||||
const double e = std::exp(-dx * dx * inv_2sig2);
|
||||
const double e = std::exp(-dx * dx * inv_2sig2);
|
||||
|
||||
f = A * e;
|
||||
|
||||
g[0] = e; // df/dA
|
||||
g[1] = 2.0 * A * e * dx * inv_2sig2; // df/dmu = A*e*(x-mu)/sig^2
|
||||
g[2] = 2.0 * A * e * dx * dx * inv_2sig2 / sig; // df/dsigma = A*e*(x-mu)^2/sig^3
|
||||
g[0] = e; // df/dA
|
||||
g[1] = 2.0 * A * e * dx * inv_2sig2; // df/dmu = A*e*(x-mu)/sig^2
|
||||
g[2] = 2.0 * A * e * dx * dx * inv_2sig2 /
|
||||
sig; // df/dsigma = A*e*(x-mu)^2/sig^3
|
||||
}
|
||||
|
||||
/** @brief Reject degenerate sigma (zero width). */
|
||||
static bool is_valid(const std::vector<double>& par) {
|
||||
static bool is_valid(const std::vector<double> &par) {
|
||||
return par[2] != 0.0;
|
||||
}
|
||||
|
||||
@@ -294,41 +301,45 @@ struct Gaussian {
|
||||
* sigma = FWHM / 2.35
|
||||
*/
|
||||
static std::array<double, npar> estimate_par(NDView<double, 1> x,
|
||||
NDView<double, 1> y)
|
||||
{
|
||||
NDView<double, 1> y) {
|
||||
// find peak
|
||||
const auto max_it = std::max_element(y.begin(), y.end());
|
||||
const ssize_t i_max = std::distance(y.begin(), max_it);
|
||||
|
||||
const double A = *max_it;
|
||||
const double A = *max_it;
|
||||
const double mu = x[i_max];
|
||||
|
||||
|
||||
// FWHM estimate
|
||||
const double half = A * 0.5;
|
||||
double x_lo = mu, x_hi = mu;
|
||||
for (ssize_t i = i_max; i >= 0; --i)
|
||||
if (y[i] < half) { x_lo = x[i]; break; }
|
||||
if (y[i] < half) {
|
||||
x_lo = x[i];
|
||||
break;
|
||||
}
|
||||
for (ssize_t i = i_max; i < y.size(); ++i)
|
||||
if (y[i] < half) { x_hi = x[i]; break; }
|
||||
if (y[i] < half) {
|
||||
x_hi = x[i];
|
||||
break;
|
||||
}
|
||||
const double sig = std::max((x_hi - x_lo) / 2.35, 1e-6);
|
||||
|
||||
|
||||
return {A, mu, sig};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Data-driven Minuit step sizes.
|
||||
*/
|
||||
static void compute_steps(const std::array<double, npar>& start,
|
||||
double x_range, double y_range, double /*slope_scale*/,
|
||||
std::array<double, npar>& steps)
|
||||
{
|
||||
static void compute_steps(const std::array<double, npar> &start,
|
||||
double x_range, double y_range,
|
||||
double /*slope_scale*/,
|
||||
std::array<double, npar> &steps) {
|
||||
steps[0] = std::max(0.1 * std::abs(start[0]), 0.1 * y_range);
|
||||
steps[1] = 0.05 * x_range;
|
||||
steps[2] = 0.05 * x_range;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// _____________________________________________________________________
|
||||
//
|
||||
// RisingScurve
|
||||
@@ -338,15 +349,16 @@ struct Gaussian {
|
||||
* @brief Rising S-curve (error-function step) with linear baseline and
|
||||
* post-step slope.
|
||||
*
|
||||
* f(x) = (p0 + p1*x) + 0.5*(1 + erf((x - p2) / (sqrt(2)*p3))) * (p4 + p5*(x - p2))
|
||||
* f(x) = (p0 + p1*x) + 0.5*(1 + erf((x - mu) / (sqrt(2)*sigma))) * (A + C*(x -
|
||||
* mu)))
|
||||
*
|
||||
* Parameters:
|
||||
* par[0] = p0 (baseline offset)
|
||||
* par[1] = p1 (baseline slope)
|
||||
* par[2] = p2 (threshold / inflection point)
|
||||
* par[3] = p3 (transition width, must be > 0)
|
||||
* par[4] = p4 (step amplitude)
|
||||
* par[5] = p5 (post-step slope)
|
||||
* par[2] = mu (inflection point)
|
||||
* par[3] = sigma (transition width, must be > 0)
|
||||
* par[4] = A (step amplitude)
|
||||
* par[5] = C (post-step slope)
|
||||
*/
|
||||
struct RisingScurve {
|
||||
static constexpr std::size_t npar = 6;
|
||||
@@ -354,13 +366,13 @@ struct RisingScurve {
|
||||
static constexpr std::array<ParamInfo, npar> param_info = {{
|
||||
{"p0", -no_bound, no_bound},
|
||||
{"p1", -no_bound, no_bound},
|
||||
{"p2", -no_bound, no_bound},
|
||||
{"p3", 1e-12, no_bound},
|
||||
{"p4", -no_bound, no_bound},
|
||||
{"p5", -no_bound, no_bound},
|
||||
{"mu", -no_bound, no_bound},
|
||||
{"sigma", 1e-12, no_bound},
|
||||
{"A", -no_bound, no_bound},
|
||||
{"C", -no_bound, no_bound},
|
||||
}};
|
||||
|
||||
static double eval(double x, const std::vector<double>& par) {
|
||||
static double eval(double x, const std::vector<double> &par) {
|
||||
const double p0 = par[0];
|
||||
const double p1 = par[1];
|
||||
const double p2 = par[2];
|
||||
@@ -368,8 +380,8 @@ struct RisingScurve {
|
||||
const double p4 = par[4];
|
||||
const double p5 = par[5];
|
||||
|
||||
const double dx = x - p2;
|
||||
const double step = 0.5 * (1.0 + std::erf(dx * inv_sqrt2 / p3));
|
||||
const double dx = x - p2;
|
||||
const double step = 0.5 * (1.0 + fast_erf(dx * inv_sqrt2 / p3));
|
||||
return (p0 + p1 * x) + step * (p4 + p5 * dx);
|
||||
}
|
||||
|
||||
@@ -381,11 +393,8 @@ struct RisingScurve {
|
||||
* dS/dp2 = -(1/sqrt(2*pi)) * exp(-z^2) / p3
|
||||
* dS/dp3 = -(1/sqrt(2*pi)) * exp(-z^2) * (x-p2) / p3^2
|
||||
*/
|
||||
static void eval_and_grad(double x,
|
||||
const std::vector<double>& par,
|
||||
double& f,
|
||||
std::array<double, npar>& g)
|
||||
{
|
||||
static void eval_and_grad(double x, const std::vector<double> &par,
|
||||
double &f, std::array<double, npar> &g) {
|
||||
const double p0 = par[0];
|
||||
const double p1 = par[1];
|
||||
const double p2 = par[2];
|
||||
@@ -393,50 +402,48 @@ struct RisingScurve {
|
||||
const double p4 = par[4];
|
||||
const double p5 = par[5];
|
||||
|
||||
const double dx = x - p2;
|
||||
const double z = dx * inv_sqrt2 / p3;
|
||||
const double step = 0.5 * (1.0 + std::erf(z));
|
||||
const double amp = p4 + p5 * dx;
|
||||
const double dx = x - p2;
|
||||
const double z = dx * inv_sqrt2 / p3;
|
||||
const double step = 0.5 * (1.0 + fast_erf(z));
|
||||
const double amp = p4 + p5 * dx;
|
||||
|
||||
f = (p0 + p1 * x) + step * amp;
|
||||
|
||||
const double e = std::exp(-z * z);
|
||||
const double e = std::exp(-z * z);
|
||||
const double dSdp2 = -inv_sqrt_2pi * e / p3;
|
||||
const double dSdp3 = -inv_sqrt_2pi * e * dx / (p3 * p3);
|
||||
|
||||
g[0] = 1.0; // df/dp0
|
||||
g[1] = x; // df/dp1
|
||||
g[2] = dSdp2 * amp - step * p5; // df/dp2
|
||||
g[3] = dSdp3 * amp; // df/dp3
|
||||
g[4] = step; // df/dp4
|
||||
g[5] = step * dx; // df/dp5
|
||||
g[0] = 1.0; // df/dp0
|
||||
g[1] = x; // df/dp1
|
||||
g[2] = dSdp2 * amp - step * p5; // df/dp2
|
||||
g[3] = dSdp3 * amp; // df/dp3
|
||||
g[4] = step; // df/dp4
|
||||
g[5] = step * dx; // df/dp5
|
||||
}
|
||||
|
||||
/** @brief Reject degenerate width (zero transition width). */
|
||||
static bool is_valid(const std::vector<double>& par) {
|
||||
static bool is_valid(const std::vector<double> &par) {
|
||||
return par[3] != 0.0;
|
||||
}
|
||||
|
||||
|
||||
/** @brief Data-driven initial parameter estimates for a rising S-curve. */
|
||||
static std::array<double, npar> estimate_par(NDView<double, 1> x,
|
||||
NDView<double, 1> y)
|
||||
{
|
||||
NDView<double, 1> y) {
|
||||
const ssize_t n = y.size();
|
||||
|
||||
// baseline: average of first ~10% of points (before turn-on)
|
||||
ssize_t n_base = std::max<ssize_t>(n / 10, 2);
|
||||
double sum_y = 0, sum_xy = 0, sum_x = 0, sum_x2 = 0;
|
||||
for (ssize_t i = 0; i < n_base; ++i) {
|
||||
sum_y += y[i];
|
||||
sum_x += x[i];
|
||||
sum_y += y[i];
|
||||
sum_x += x[i];
|
||||
sum_xy += x[i] * y[i];
|
||||
sum_x2 += x[i] * x[i];
|
||||
}
|
||||
double denom = n_base * sum_x2 - sum_x * sum_x;
|
||||
double p1 = (std::abs(denom) > 1e-30)
|
||||
? (n_base * sum_xy - sum_x * sum_y) / denom
|
||||
: 0.0;
|
||||
? (n_base * sum_xy - sum_x * sum_y) / denom
|
||||
: 0.0;
|
||||
double p0 = (sum_y - p1 * sum_x) / n_base;
|
||||
|
||||
// plateau: average of last ~10%
|
||||
@@ -466,10 +473,16 @@ struct RisingScurve {
|
||||
double y_90 = baseline_at_mid + 0.9 * p4;
|
||||
double x_10 = x[0], x_90 = x[n - 1];
|
||||
for (ssize_t i = 0; i < n; ++i) {
|
||||
if (y[i] >= y_10) { x_10 = x[i]; break; }
|
||||
if (y[i] >= y_10) {
|
||||
x_10 = x[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (ssize_t i = 0; i < n; ++i) {
|
||||
if (y[i] >= y_90) { x_90 = x[i]; break; }
|
||||
if (y[i] >= y_90) {
|
||||
x_90 = x[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
// for a Gaussian CDF: 10%-90% width = 2 * 1.2816 * sigma
|
||||
double p3 = std::max((x_90 - x_10) / 2.5631, 1.0);
|
||||
@@ -478,11 +491,11 @@ struct RisingScurve {
|
||||
|
||||
return {p0, p1, p2, p3, p4, p5};
|
||||
}
|
||||
|
||||
static void compute_steps(const std::array<double, npar>& start,
|
||||
double x_range, double y_range, double slope_scale,
|
||||
std::array<double, npar>& steps)
|
||||
{
|
||||
|
||||
static void compute_steps(const std::array<double, npar> &start,
|
||||
double x_range, double y_range,
|
||||
double slope_scale,
|
||||
std::array<double, npar> &steps) {
|
||||
steps[0] = std::max(0.1 * std::abs(start[0]), 0.1 * y_range);
|
||||
steps[1] = 0.1 * slope_scale;
|
||||
steps[2] = 0.05 * x_range;
|
||||
@@ -492,7 +505,6 @@ struct RisingScurve {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// _____________________________________________________________________
|
||||
//
|
||||
// FallingScurve
|
||||
@@ -502,7 +514,8 @@ struct RisingScurve {
|
||||
* @brief Falling S-curve (complementary error-function step) with linear
|
||||
* baseline and post-step slope.
|
||||
*
|
||||
* f(x) = (p0 + p1*x) + 0.5*(1 - erf((x - p2) / (sqrt(2)*p3))) * (p4 + p5*(x - p2))
|
||||
* f(x) = (p0 + p1*x) + 0.5*(1 - erf((x - mu) / (sqrt(2)*sigma))) * (A + C*(x -
|
||||
* mu))
|
||||
*
|
||||
* Parameters are identical to RisingScurve. The only difference is the
|
||||
* sign of the erf term, which flips the step direction (and the signs of
|
||||
@@ -514,13 +527,13 @@ struct FallingScurve {
|
||||
static constexpr std::array<ParamInfo, npar> param_info = {{
|
||||
{"p0", -no_bound, no_bound},
|
||||
{"p1", -no_bound, no_bound},
|
||||
{"p2", -no_bound, no_bound},
|
||||
{"p3", 1e-12, no_bound},
|
||||
{"p4", -no_bound, no_bound},
|
||||
{"p5", -no_bound, no_bound},
|
||||
{"mu", -no_bound, no_bound},
|
||||
{"sigma", 1e-12, no_bound},
|
||||
{"A", -no_bound, no_bound},
|
||||
{"C", -no_bound, no_bound},
|
||||
}};
|
||||
|
||||
static double eval(double x, const std::vector<double>& par) {
|
||||
static double eval(double x, const std::vector<double> &par) {
|
||||
const double p0 = par[0];
|
||||
const double p1 = par[1];
|
||||
const double p2 = par[2];
|
||||
@@ -528,16 +541,13 @@ struct FallingScurve {
|
||||
const double p4 = par[4];
|
||||
const double p5 = par[5];
|
||||
|
||||
const double dx = x - p2;
|
||||
const double step = 0.5 * (1.0 - std::erf(dx * inv_sqrt2 / p3));
|
||||
const double dx = x - p2;
|
||||
const double step = 0.5 * (1.0 - fast_erf(dx * inv_sqrt2 / p3));
|
||||
return (p0 + p1 * x) + step * (p4 + p5 * dx);
|
||||
}
|
||||
|
||||
static void eval_and_grad(double x,
|
||||
const std::vector<double>& par,
|
||||
double& f,
|
||||
std::array<double, npar>& g)
|
||||
{
|
||||
static void eval_and_grad(double x, const std::vector<double> &par,
|
||||
double &f, std::array<double, npar> &g) {
|
||||
const double p0 = par[0];
|
||||
const double p1 = par[1];
|
||||
const double p2 = par[2];
|
||||
@@ -545,15 +555,15 @@ struct FallingScurve {
|
||||
const double p4 = par[4];
|
||||
const double p5 = par[5];
|
||||
|
||||
const double dx = x - p2;
|
||||
const double z = dx * inv_sqrt2 / p3;
|
||||
const double step = 0.5 * (1.0 - std::erf(z));
|
||||
const double amp = p4 + p5 * dx;
|
||||
const double dx = x - p2;
|
||||
const double z = dx * inv_sqrt2 / p3;
|
||||
const double step = 0.5 * (1.0 - fast_erf(z));
|
||||
const double amp = p4 + p5 * dx;
|
||||
|
||||
f = (p0 + p1 * x) + step * amp;
|
||||
|
||||
const double e = std::exp(-z * z);
|
||||
const double dSdp2 = +inv_sqrt_2pi * e / p3; // sign flipped vs rising
|
||||
const double e = std::exp(-z * z);
|
||||
const double dSdp2 = +inv_sqrt_2pi * e / p3; // sign flipped vs rising
|
||||
const double dSdp3 = +inv_sqrt_2pi * e * dx / (p3 * p3);
|
||||
|
||||
g[0] = 1.0;
|
||||
@@ -565,29 +575,28 @@ struct FallingScurve {
|
||||
}
|
||||
|
||||
/** @brief Reject degenerate width (zero transition width). */
|
||||
static bool is_valid(const std::vector<double>& par) {
|
||||
static bool is_valid(const std::vector<double> &par) {
|
||||
return par[3] != 0.0;
|
||||
}
|
||||
|
||||
/** @brief Data-driven initial parameter estimates for a falling S-curve. */
|
||||
static std::array<double, npar> estimate_par(NDView<double, 1> x,
|
||||
NDView<double, 1> y)
|
||||
{
|
||||
NDView<double, 1> y) {
|
||||
const ssize_t n = y.size();
|
||||
|
||||
// baseline: last ~10% of points (after turn-off)
|
||||
ssize_t n_base = std::max<ssize_t>(n / 10, 2);
|
||||
double sum_y = 0, sum_xy = 0, sum_x = 0, sum_x2 = 0;
|
||||
for (ssize_t i = n - n_base; i < n; ++i) {
|
||||
sum_y += y[i];
|
||||
sum_x += x[i];
|
||||
sum_y += y[i];
|
||||
sum_x += x[i];
|
||||
sum_xy += x[i] * y[i];
|
||||
sum_x2 += x[i] * x[i];
|
||||
}
|
||||
double denom = n_base * sum_x2 - sum_x * sum_x;
|
||||
double p1 = (std::abs(denom) > 1e-30)
|
||||
? (n_base * sum_xy - sum_x * sum_y) / denom
|
||||
: 0.0;
|
||||
? (n_base * sum_xy - sum_x * sum_y) / denom
|
||||
: 0.0;
|
||||
double p0 = (sum_y - p1 * sum_x) / n_base;
|
||||
|
||||
// plateau: average of first ~10%
|
||||
@@ -617,10 +626,16 @@ struct FallingScurve {
|
||||
double y_10 = baseline_at_mid + 0.1 * p4;
|
||||
double x_90 = x[0], x_10 = x[n - 1];
|
||||
for (ssize_t i = 0; i < n; ++i) {
|
||||
if (y[i] <= y_90) { x_90 = x[i]; break; }
|
||||
if (y[i] <= y_90) {
|
||||
x_90 = x[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (ssize_t i = 0; i < n; ++i) {
|
||||
if (y[i] <= y_10) { x_10 = x[i]; break; }
|
||||
if (y[i] <= y_10) {
|
||||
x_10 = x[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
// same CDF relationship: 10%-90% width = 2 * 1.2816 * sigma
|
||||
double p3 = std::max((x_10 - x_90) / 2.5631, 1.0);
|
||||
@@ -629,12 +644,13 @@ struct FallingScurve {
|
||||
|
||||
return {p0, p1, p2, p3, p4, p5};
|
||||
}
|
||||
|
||||
static void compute_steps(const std::array<double, npar>& start,
|
||||
double x_range, double y_range, double slope_scale,
|
||||
std::array<double, npar>& steps)
|
||||
{
|
||||
RisingScurve::compute_steps(start, x_range, y_range, slope_scale, steps);
|
||||
|
||||
static void compute_steps(const std::array<double, npar> &start,
|
||||
double x_range, double y_range,
|
||||
double slope_scale,
|
||||
std::array<double, npar> &steps) {
|
||||
RisingScurve::compute_steps(start, x_range, y_range, slope_scale,
|
||||
steps);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class NDArray : public ArrayExpr<NDArray<T, Ndim>, Ndim> {
|
||||
* @brief Default constructor. Constructs an empty NDArray.
|
||||
*
|
||||
*/
|
||||
NDArray() : shape_(), strides_(c_strides<Ndim>(shape_)), data_(nullptr){};
|
||||
NDArray() : shape_(), strides_(c_strides<Ndim>(shape_)), data_(nullptr) {};
|
||||
|
||||
/**
|
||||
* @brief Construct a new NDArray object with a given shape.
|
||||
|
||||
@@ -63,7 +63,7 @@ template <class T> struct ProducerConsumerQueue {
|
||||
return *this;
|
||||
}
|
||||
|
||||
ProducerConsumerQueue() : ProducerConsumerQueue(2){};
|
||||
ProducerConsumerQueue() : ProducerConsumerQueue(2) {};
|
||||
// size must be >= 2.
|
||||
//
|
||||
// Also, note that the number of usable slots in the queue at any
|
||||
|
||||
@@ -67,7 +67,7 @@ class Logger {
|
||||
|
||||
public:
|
||||
Logger() = default;
|
||||
explicit Logger(TLogLevel level) : m_level(level){};
|
||||
explicit Logger(TLogLevel level) : m_level(level) {};
|
||||
~Logger() {
|
||||
// output in the destructor to allow for << syntax
|
||||
os << RESET << '\n';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#pragma once
|
||||
#include "aare/NDView.hpp"
|
||||
#include "aare/utils/task.hpp"
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "aare/NDView.hpp"
|
||||
#include "aare/utils/task.hpp"
|
||||
|
||||
namespace aare {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user