From 7ef2936b4f1bc3305202cd7de86c5ae42334618b Mon Sep 17 00:00:00 2001 From: Cedric Vallee Date: Wed, 27 May 2026 17:16:51 +0100 Subject: [PATCH 1/3] Added joblib dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index f756133..37df525 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "matplotlib", "jupyterlab", "seaborn", + "joblib", ] [tool.setuptools.packages.find] From b9aeb9a2d3aa8a326cacc14dcb2857332e6ae6d8 Mon Sep 17 00:00:00 2001 From: Cedric Vallee Date: Wed, 27 May 2026 17:52:31 +0100 Subject: [PATCH 2/3] Added Chi2 and Sqrt(Chi2) GoF for ODE --- sensofit/batch.py | 2 ++ sensofit/ode_fitting.py | 25 +++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/sensofit/batch.py b/sensofit/batch.py index 3490aaf..28e5c78 100644 --- a/sensofit/batch.py +++ b/sensofit/batch.py @@ -186,6 +186,7 @@ def _extract_row(sample, result, mode): 'Rmax': result['Rmax'], 'KD': result['KD'], 'KD_uM': result['KD'] * 1e6, + 'Sqrt(Chi2)': result.get('Sqrt(Chi2)', np.nan), 'ka_se': result.get('ka_se', np.nan), 'kd_se': result.get('kd_se', np.nan), 'Rmax_se': result.get('Rmax_se', np.nan), @@ -226,6 +227,7 @@ def _fallback_row(sample, mode): 'Rmax': np.nan, 'KD': np.nan, 'KD_uM': np.nan, + 'Sqrt(Chi2)': np.nan, 'sigma_res': np.nan, 'n_points': 0, 'fit_mode': mode, diff --git a/sensofit/ode_fitting.py b/sensofit/ode_fitting.py index 5cfac40..31a972a 100644 --- a/sensofit/ode_fitting.py +++ b/sensofit/ode_fitting.py @@ -44,6 +44,21 @@ def _residuals_full(params, t, signal, c_func, w): R_sim = simulate_sensorgram(t, ka, kd, Rmax, c_func, R0=0.0) return w * (signal - R_sim) +def _chi2(residuals=None, w=None, n_params=None, R_sim=None, signal=None, params=None, t=None, c_func=None, sqrt=False): + """Calculate Chi2 from ODE residuals. + + Chi2 = sum((w * residuals)^2) / (N - n_params) + Returns Chi2 or Sqrt(Chi2) if sqrt=True.""" + if not residuals: + if R_sim: + residuals = w * (signal - R_sim) + else: + residuals = _residuals_full(params, t, signal, c_func, w) + + n_points = int((w > 0).sum()) if w is not None else len(residuals) + chi2 = np.sum(residuals**2)/(n_points - max(n_params, 1)) + return np.sqrt(chi2) if sqrt else chi2 + def _solve_R0_Rss(kd, t_dissoc, signal_dissoc, t0): """Closed-form linear regression for R0 and Rss given fixed kd. @@ -151,9 +166,11 @@ def ode_fit(t, signal, c_func, w, markers, ka0, kd0, Rmax0, if not fits: # Fallback: use derived estimates R_fit = simulate_sensorgram(t, ka_est, kd_final, Rmax_est, c_func, R0=0.0) + sqrt_chi2 = _chi2(R_sim=R_fit, n_params=len([ka_est, kd_final, Rmax_est]), w=w, sqrt=True) return { 'ka': ka_est, 'kd': kd_final, 'Rmax': Rmax_est, 'KD': kd_final / ka_est, + 'Sqrt(Chi2)': sqrt_chi2, 'R0': R0_est, 'Rss': Rss_est, 'ka_se': np.nan, 'kd_se': np.nan, 'Rmax_se': np.nan, 'cov': np.full((3, 3), np.nan), @@ -186,9 +203,10 @@ def ode_fit(t, signal, c_func, w, markers, ka0, kd0, Rmax0, # Confidence from best Jacobian (lowest cost) best_idx = np.argmin([f[1] for f in fits]) best_jac = fits[best_idx][2] - + + params = [ka_final_val, kd_final_val, Rmax_final] residuals = _residuals_full( - [ka_final_val, kd_final_val, Rmax_final], t, signal, c_func, w) + params, t, signal, c_func, w) n = int((w > 0).sum()) dof = max(n - 3, 1) sigma2 = np.sum(residuals ** 2) / dof @@ -206,11 +224,14 @@ def ode_fit(t, signal, c_func, w, markers, ka0, kd0, Rmax0, R_fit = simulate_sensorgram(t, ka_final_val, kd_final_val, Rmax_final, c_func, R0=0.0) + sqrt_chi2 = _chi2(residuals=residuals, n_params=len(params), w=w, sqrt=True) + return { 'ka': ka_final_val, 'kd': kd_final_val, 'Rmax': Rmax_final, 'KD': KD, + 'Sqrt(Chi2)': sqrt_chi2, 'R0': R0_est, 'Rss': Rss_est, 'ka_se': ka_se, From af79201942c28d08fdfcc499d5e36acea3519681 Mon Sep 17 00:00:00 2001 From: Cedric Vallee Date: Thu, 28 May 2026 11:58:25 +0100 Subject: [PATCH 3/3] Fixing error from `_chi2()` --- sensofit/ode_fitting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sensofit/ode_fitting.py b/sensofit/ode_fitting.py index 31a972a..499a895 100644 --- a/sensofit/ode_fitting.py +++ b/sensofit/ode_fitting.py @@ -49,8 +49,8 @@ def _chi2(residuals=None, w=None, n_params=None, R_sim=None, signal=None, params Chi2 = sum((w * residuals)^2) / (N - n_params) Returns Chi2 or Sqrt(Chi2) if sqrt=True.""" - if not residuals: - if R_sim: + if residuals is None: + if R_sim is not None: residuals = w * (signal - R_sim) else: residuals = _residuals_full(params, t, signal, c_func, w)