Skip to content

Commit d0803f9

Browse files
author
Vittoria Tommasini
committed
Add LOO and plotting diagnostics and unit tests
Simplified parallelization and tests, and decreased number of test_data points to make tests run faster
1 parent 4116073 commit d0803f9

4 files changed

Lines changed: 424 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .core import *
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
# Distributed under the MIT License.
2+
# See LICENSE.txt for details.
3+
4+
"""
5+
Gaussian Process Regression machine learning diagnostic functions library.
6+
Contains all the functions necessary to validate and plot the GPR model used
7+
to predict better low-eccentricity orbital parameter initial guesses.
8+
"""
9+
10+
import logging
11+
import os
12+
13+
import matplotlib.pyplot as plt
14+
import numpy as np
15+
import torch
16+
17+
os.environ["OMP_NUM_THREADS"] = "1"
18+
from multiprocessing import Pool
19+
20+
from SimulationSupport.gpr import predict_with_gpr_model, train_gpr_model
21+
22+
logger = logging.getLogger(__name__)
23+
24+
25+
# Leave-one-out parallelization set up
26+
def _loo_single(i, X, Y):
27+
"""
28+
Run a single LOO iteration for index i.
29+
30+
Trains a GPR on all points except index i, then predicts the held-out point.
31+
Called in parallel by loo_crossval.
32+
33+
Args:
34+
i (int): Index of the held-out point
35+
X (np.ndarray): Input features, with shape (N, D)
36+
Y (np.ndarray): Target variable, with shape (N, )
37+
38+
Returns:
39+
i (int): Index of the held-out point
40+
pred_mean (float): Predicted mean for the held-out point
41+
pred_std (float): Predicted std dev for the held-out point
42+
"""
43+
N = len(Y)
44+
45+
# Create train and test split
46+
# Boolean mask: all True except index i (held out point)
47+
train_mask = np.ones(N, dtype=bool)
48+
train_mask[i] = False
49+
50+
X_train = X[train_mask]
51+
Y_train = Y[train_mask]
52+
# Slice preserves the 2D shape expected by the model
53+
X_test = X[i : i + 1]
54+
55+
# Train and predict
56+
model_loo, likelihood_loo = train_gpr_model(X_train, Y_train)
57+
pred_mean, pred_std = predict_with_gpr_model(
58+
X_test, model_loo, likelihood_loo
59+
)
60+
61+
return i, pred_mean[0], pred_std[0]
62+
63+
64+
# Leave-one-out cross-validation
65+
def loo_crossval(
66+
X: np.ndarray,
67+
Y: np.ndarray,
68+
target_name="Target",
69+
n_jobs=-1,
70+
):
71+
"""
72+
Perform Leave-One-Out Cross-Validation for a GPR model.
73+
74+
Trains N models (each omitting one point), predicts the held-out point,
75+
collect predictions and uncertainties, and then computes and plots summary metrics.
76+
This gives an unbiased estimate of generalization performance.
77+
78+
Args:
79+
X (np.ndarray): Input features, with shape (N, D)
80+
Y (np.ndarray): Target variable, with shape (N, )
81+
target_name (str): Label for plots and print outputs
82+
n_jobs (int): Number of parallel workers (-1 uses all available cores)
83+
84+
Returns:
85+
predictions_loo (np.ndarray): LOO predicted values, with shape (N, )
86+
uncertainties_loo (np.ndarray): LOO predicted std devs, with shape (N, )
87+
"""
88+
N = len(Y)
89+
predictions_loo = np.zeros_like(Y)
90+
uncertainties_loo = np.zeros_like(Y)
91+
92+
# Force single worker when GPU is used as parallel processes can't share a GPU
93+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
94+
if device.type == "cuda":
95+
n_jobs = 1
96+
97+
# multiprocessing.Pool uses None to use all available cores
98+
pool_size = None if n_jobs == -1 else n_jobs
99+
100+
logger.info(
101+
f"Processing {N} LOO iterations for {target_name} using"
102+
f" {pool_size or os.cpu_count()} parallel workers..."
103+
)
104+
105+
# Build argument tuples for each LOO fold
106+
# because pool.starmap needs one tuple of positional args per call
107+
args = [(i, X, Y) for i in range(N)]
108+
109+
if n_jobs == 1:
110+
# Run sequentially when there is a single worker
111+
results = [_loo_single(*a) for a in args]
112+
else:
113+
# Run LOO folds in parallel with multiple workers
114+
with Pool(processes=pool_size) as pool:
115+
results = pool.starmap(_loo_single, args)
116+
117+
# Unpack results back into the predictions and uncertainties arrays
118+
for i, pred_mean, pred_std in results:
119+
predictions_loo[i] = pred_mean
120+
uncertainties_loo[i] = pred_std
121+
122+
return predictions_loo, uncertainties_loo
123+
124+
125+
def plot_loo_crossval(Y, predictions_loo, target_name="Target", plot=False):
126+
"""
127+
Compute summary statistics for Leave-One-Out Cross-Validation results, and optionally
128+
plot the correlation.
129+
130+
Args:
131+
Y (np.ndarray): True target values, with shape (N, )
132+
predictions_loo (np.ndarray): LOO predicted values, with shape (N, )
133+
target_name (str): Label for plots and log outputs
134+
plot (bool): Whether to produce a correlation plot. Default is False.
135+
136+
Returns:
137+
rmse_loo (float): Root mean squared error of the LOO predictions
138+
mae_loo (float): Mean absolute error of the LOO predictions
139+
r_squared_loo (float): R^2 computed from the Pearson correlation
140+
"""
141+
142+
Y_loo = Y # Same as the original Y for the multi input case
143+
144+
# Calculate metrics - always computed, regardless of whether plot is requested
145+
# R^2 is computed from the Pearson correlation coefficient -
146+
# equivalent to the coefficient of determination for a linear fit through the origin
147+
correlation = np.corrcoef(Y_loo, predictions_loo)[0, 1]
148+
r_squared_loo = correlation**2
149+
150+
# Metrics with goal values
151+
rmse_loo = np.sqrt(np.mean((Y_loo - predictions_loo) ** 2))
152+
mae_loo = np.mean(np.abs(Y_loo - predictions_loo))
153+
y_range = Y_loo.max() - Y_loo.min()
154+
155+
# Plot correlation
156+
if plot:
157+
plt.figure(figsize=(8, 6))
158+
plt.scatter(Y_loo, predictions_loo, alpha=0.6, s=20)
159+
160+
# y = x reference line: perfect predictions would lie exactly on this line
161+
min_val = min(Y_loo.min(), predictions_loo.min())
162+
max_val = max(Y_loo.max(), predictions_loo.max())
163+
plt.plot(
164+
[min_val, max_val],
165+
[min_val, max_val],
166+
"r--",
167+
lw=2,
168+
label="Perfect Correlation",
169+
)
170+
171+
# Labels and formatting
172+
plt.xlabel(f"True Δ{target_name}", fontsize=12)
173+
plt.ylabel(f"LOO Predicted Δ{target_name}", fontsize=12)
174+
plt.title(f"LOO: GPR Predictions vs True ({target_name})", fontsize=14)
175+
plt.grid(True, alpha=0.3)
176+
plt.legend()
177+
178+
# Display R^2
179+
plt.text(
180+
0.95,
181+
0.95,
182+
f"R² = {r_squared_loo:.4f}",
183+
transform=plt.gca().transAxes,
184+
fontsize=12,
185+
bbox=dict(boxstyle="round", facecolor="white", alpha=0.8),
186+
horizontalalignment="right",
187+
)
188+
plt.tight_layout()
189+
plt.show()
190+
191+
# Log metrics with goal values for quick analysis
192+
logger.info(f"Leave-one-out Cross Validation Results ({target_name})")
193+
logger.info(
194+
f"RMSE: {rmse_loo:.6f} ({100 * rmse_loo / y_range:.6f}% of target"
195+
" range; goal: < 1 % of target range, lower is better)"
196+
)
197+
logger.info(
198+
f"MAE: {mae_loo:.6f} ({100 * mae_loo / y_range:.6f}% of target range;"
199+
" goal: < 1 % of target range, lower is better)"
200+
)
201+
logger.info(
202+
f"R²: {r_squared_loo:.4f} (goal: > 0.95 excellent, > 0.90 good, < 0.70"
203+
" poor)"
204+
)
205+
logger.info(f"\n Dataset size: {len(Y_loo)} points")
206+
logger.info(
207+
f"Each model is trained on {len(Y_loo)-1} points, and tested on 1 point"
208+
)
209+
logger.info("This provides an unbiased generalization estimate.")
210+
211+
return rmse_loo, mae_loo, r_squared_loo
212+
213+
214+
# Leave-one-out residual computation and plotting
215+
def plot_loo_residuals(Y_loo, predictions_loo, target_name="Target", show=True):
216+
"""
217+
Calculate LOO prediction residuals, plot a histogram, and print summary statistics.
218+
219+
Args:
220+
Y_loo (np.ndarray): True target values from LOO cross validation
221+
predictions_loo (np.ndarray): Predicted values from LOO cross validation
222+
target_name (str): Name of target variable
223+
show (bool): Whether to produce a residual plot. Default is True.
224+
225+
Returns:
226+
residuals_loo (np.ndarray): Per-point residuals (true - predicted), with shape (N,)
227+
228+
"""
229+
230+
# Compute residuals: LOO prediction error
231+
# Residual = true - predicted
232+
residuals_loo = Y_loo - predictions_loo
233+
234+
# Make histogram
235+
plt.figure(figsize=(8, 5))
236+
plt.hist(residuals_loo, bins=20, color="skyblue", edgecolor="k", alpha=0.8)
237+
# Vertical line at zero: residuals centered here indicate no systematic bias;
238+
# ideally the histogram is centered on this line; a shifted distribution suggests
239+
# the model is over- or under-predicting
240+
plt.axvline(0, color="r", linestyle="--", label="Zero Error")
241+
242+
plt.title(f"LOO Residuals Histogram for {target_name}")
243+
plt.xlabel(" Residuals", fontsize=16)
244+
plt.ylabel("Count", fontsize=16)
245+
plt.tick_params(axis="both", which="major", labelsize=14)
246+
plt.grid(True, alpha=0.3)
247+
plt.legend(fontsize=14)
248+
plt.tight_layout()
249+
if show:
250+
plt.show()
251+
252+
# Print statistics
253+
logger.info(f"Residual statistics for {target_name}:")
254+
logger.info(f"Mean residual: {np.mean(residuals_loo):.4e}")
255+
logger.info(f"Std of residuals: {np.std (residuals_loo):.4e}")
256+
logger.info(f"Max residual: {np.max (residuals_loo):.4e}")
257+
logger.info(f"Min residual: {np.min (residuals_loo):.4e}")
258+
259+
return residuals_loo

tests/test_data.csv

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name,initial_separation,initial_orbital_frequency,mass_ratio,S1x,S1y,S1z,S2x,S2y,S2z,eccentricity,spec_pn_guess_omega,spec_pn_guess_adot,initial_adot
2+
0001/Lev3,15.146789551,0.014983690465,7.999999918,-0.444100023,0.360385319,-0.55936592,0.071805115,0.03265513,0.796101492,0.0005075,0.01559885154938874,-2.2414845982581062e-05,0.0003528012285305
3+
0002/Lev3,14.579101562,0.015942305459,8.000000143,0.0,-0.0,-6e-09,-0.347751519,0.445159158,0.566476988,0.0002896,0.016341629394498167,-2.3680485553121006e-05,0.000159105934009
4+
0004/Lev3,15.503967285,0.014706941894,8.000000481,0.000303903,-0.011969266,-0.799910513,-0.739600217,0.30492019,-0.000120588,0.0003987,0.015174878963317045,-2.154406898560398e-05,-0.0001587184996252
5+
0005/Lev3,15.438903809,0.014654531914,7.999999181,0.00298581,-0.008448617,-0.79994983,-0.564500553,0.039031391,0.565516572,2.77e-05,0.015240810159895427,-2.1666522793373512e-05,0.0002543673632399
6+
0006/Lev3,14.579101562,0.015769421025,7.999999181,-4e-09,-3e-09,-8e-09,0.559506731,0.079097834,0.566294787,0.0001282,0.016341637406258692,-2.368057939907583e-05,0.0003752964487323
7+
0007/Lev3,15.50390625,0.014664690696,8.000000665,0.006875942,-0.010908533,-0.799896075,-0.786638905,-0.145546247,-0.000571885,0.0001367,0.015174981557624414,-2.1544568569537015e-05,0.0001784599171092
8+
0009/Lev3,15.411865234,0.014663431789,8.000001632,-0.0,0.0,-0.799999967,0.0,-0.0,0.799999939,0.0003167,0.015268303660605614,-2.171661092605118e-05,0.0003501092427135
9+
0011/Lev3,15.503417969,0.015165995614,8.000000466,0.0,-0.0,-0.80000003,0.0,0.0,-7.3e-08,0.0001211,0.015175642438146477,-2.1547280461839417e-05,-1.23802044759e-05
10+
0013/Lev3,14.692016602,0.015728070685,7.999999936,-0.47294438,0.644897574,0.020499971,-0.123359058,-0.029909627,-0.789865636,0.0002154,0.01621729014211344,-2.352433131065712e-05,-0.0002674027897973
11+
0014/Lev3,15.438842773,0.014654328555,7.999998854,-0.003442869,0.008343849,-0.799949112,0.565826025,-0.007280285,0.56549196,0.0001138,0.015240897307820992,-2.1666886186209923e-05,0.0002719734912783
12+
0016/Lev3,14.717041016,0.015695747179,7.999999595,-0.399260394,0.024224834,0.000720113,-0.026981,-0.050431671,-0.797952774,0.0002248,0.016183988355650523,-2.346470328035656e-05,-0.000251207486167
13+
0017/Lev3,14.583679199,0.016392407094,8.000000396,-0.488924,0.632639092,0.026825414,-0.150335172,0.78571102,-0.007498016,0.0008101,0.016348713284405895,-2.3764438021643823e-05,-6.4010430648e-05
14+
0019/Lev3,14.597961426,0.015927828051,8.000000125,0.701697968,-0.382959491,0.030856628,-0.687468417,-0.390562691,-0.121818714,0.0004568,0.016336235343449447,-2.3781535220848515e-05,0.0003163239615238
15+
0021/Lev3,14.591552734,0.01633987153,7.999999911,-0.789844032,0.125155646,0.021649661,-0.021367251,0.794098405,-0.094549603,0.0001867,0.01634449989587672,-2.381815266563059e-05,3.43913187892e-05
16+
0023/Lev3,14.511291504,0.015749219945,8.000002317,0.776143771,-0.193526348,0.011596621,-0.077528748,-0.09208033,0.790892004,0.000514,0.016436778350671272,-2.3951680093620006e-05,0.0004102526667998
17+
0024/Lev3,15.438903809,0.014860142586,8.000000454,-0.008556994,0.003099969,-0.799948226,0.332856856,0.457860363,0.565299501,8.42e-05,0.015240816486218183,-2.1666580164985422e-05,0.0004166173600393

0 commit comments

Comments
 (0)