- registration getting better
This commit is contained in:
+59
-196
@@ -13,18 +13,16 @@ where
|
|||||||
T: Float + Debug,
|
T: Float + Debug,
|
||||||
F: ObjectiveFunction<T>,
|
F: ObjectiveFunction<T>,
|
||||||
{
|
{
|
||||||
const M: usize = 10; // Number of corrections to store
|
const M: usize = 10;
|
||||||
let n = initial_point.len();
|
let n = initial_point.len();
|
||||||
let mut current_point = initial_point.to_vec();
|
let mut current_point = initial_point.to_vec();
|
||||||
let mut iterations = 0;
|
let mut iterations = 0;
|
||||||
let mut converged = false;
|
let mut converged = false;
|
||||||
|
|
||||||
// Storage for the last M corrections
|
|
||||||
let mut s_list: VecDeque<Vec<T>> = VecDeque::with_capacity(M);
|
let mut s_list: VecDeque<Vec<T>> = VecDeque::with_capacity(M);
|
||||||
let mut y_list: VecDeque<Vec<T>> = VecDeque::with_capacity(M);
|
let mut y_list: VecDeque<Vec<T>> = VecDeque::with_capacity(M);
|
||||||
let mut rho_list: VecDeque<T> = VecDeque::with_capacity(M);
|
let mut rho_list: VecDeque<T> = VecDeque::with_capacity(M);
|
||||||
|
|
||||||
// Get initial gradient
|
|
||||||
let mut gradient = match f.gradient(¤t_point) {
|
let mut gradient = match f.gradient(¤t_point) {
|
||||||
Some(g) => g,
|
Some(g) => g,
|
||||||
None => {
|
None => {
|
||||||
@@ -38,7 +36,6 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
while iterations < config.max_iterations {
|
while iterations < config.max_iterations {
|
||||||
// Check for convergence
|
|
||||||
let gradient_norm = gradient
|
let gradient_norm = gradient
|
||||||
.iter()
|
.iter()
|
||||||
.fold(T::zero(), |acc, &x| acc + x * x)
|
.fold(T::zero(), |acc, &x| acc + x * x)
|
||||||
@@ -48,11 +45,9 @@ where
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute search direction using L-BFGS two-loop recursion
|
|
||||||
let mut q = gradient.clone();
|
let mut q = gradient.clone();
|
||||||
let mut alpha_list = Vec::with_capacity(s_list.len());
|
let mut alpha_list = Vec::with_capacity(s_list.len());
|
||||||
|
|
||||||
// First loop
|
|
||||||
for i in (0..s_list.len()).rev() {
|
for i in (0..s_list.len()).rev() {
|
||||||
let alpha = rho_list[i]
|
let alpha = rho_list[i]
|
||||||
* s_list[i]
|
* s_list[i]
|
||||||
@@ -65,7 +60,6 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale the initial Hessian approximation
|
|
||||||
let mut r = if !s_list.is_empty() {
|
let mut r = if !s_list.is_empty() {
|
||||||
let i = s_list.len() - 1;
|
let i = s_list.len() - 1;
|
||||||
let yy = y_list[i].iter().fold(T::zero(), |acc, &y| acc + y * y);
|
let yy = y_list[i].iter().fold(T::zero(), |acc, &y| acc + y * y);
|
||||||
@@ -81,7 +75,6 @@ where
|
|||||||
q
|
q
|
||||||
};
|
};
|
||||||
|
|
||||||
// Second loop
|
|
||||||
for i in 0..s_list.len() {
|
for i in 0..s_list.len() {
|
||||||
let beta = rho_list[i]
|
let beta = rho_list[i]
|
||||||
* y_list[i]
|
* y_list[i]
|
||||||
@@ -94,28 +87,23 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// r now contains the search direction
|
|
||||||
let direction: Vec<T> = r.iter().map(|&x| -x).collect();
|
let direction: Vec<T> = r.iter().map(|&x| -x).collect();
|
||||||
|
|
||||||
// Line search to find step size with Armijo sufficient decrease condition
|
|
||||||
let mut alpha = T::one();
|
let mut alpha = T::one();
|
||||||
let mut new_point = vec![T::zero(); n];
|
let mut new_point = vec![T::zero(); n];
|
||||||
let current_value = f.evaluate(¤t_point);
|
let current_value = f.evaluate(¤t_point);
|
||||||
let mut improved = false;
|
let mut improved = false;
|
||||||
// directional derivative g^T * d (should be negative for descent)
|
|
||||||
let g_dot_d: T = gradient
|
let g_dot_d: T = gradient
|
||||||
.iter()
|
.iter()
|
||||||
.zip(direction.iter())
|
.zip(direction.iter())
|
||||||
.fold(T::zero(), |acc, (&g, &d)| acc + g * d);
|
.fold(T::zero(), |acc, (&g, &d)| acc + g * d);
|
||||||
let c1 = T::from(1e-4).unwrap(); // Armijo constant
|
let c1 = T::from(1e-4).unwrap();
|
||||||
|
|
||||||
// Backtracking line search with Armijo condition
|
|
||||||
for _ in 0..30 {
|
for _ in 0..30 {
|
||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
new_point[i] = current_point[i] + alpha * direction[i];
|
new_point[i] = current_point[i] + alpha * direction[i];
|
||||||
}
|
}
|
||||||
let new_value = f.evaluate(&new_point);
|
let new_value = f.evaluate(&new_point);
|
||||||
// Armijo: f(x + αd) ≤ f(x) + c₁ α (gᵀd)
|
|
||||||
if new_value <= current_value + c1 * alpha * g_dot_d {
|
if new_value <= current_value + c1 * alpha * g_dot_d {
|
||||||
improved = true;
|
improved = true;
|
||||||
break;
|
break;
|
||||||
@@ -123,18 +111,15 @@ where
|
|||||||
alpha = alpha * T::from(0.5).unwrap();
|
alpha = alpha * T::from(0.5).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIX: If no improvement found, don't update (would corrupt L-BFGS history)
|
|
||||||
if !improved {
|
if !improved {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get new gradient
|
|
||||||
let new_gradient = match f.gradient(&new_point) {
|
let new_gradient = match f.gradient(&new_point) {
|
||||||
Some(g) => g,
|
Some(g) => g,
|
||||||
None => break,
|
None => break,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the correction vectors
|
|
||||||
let s = new_point
|
let s = new_point
|
||||||
.iter()
|
.iter()
|
||||||
.zip(current_point.iter())
|
.zip(current_point.iter())
|
||||||
@@ -151,7 +136,6 @@ where
|
|||||||
.zip(s.iter())
|
.zip(s.iter())
|
||||||
.fold(T::zero(), |acc, (&y_i, &s_i)| acc + y_i * s_i);
|
.fold(T::zero(), |acc, (&y_i, &s_i)| acc + y_i * s_i);
|
||||||
|
|
||||||
// FIX: Check for non-positive ys (corrupts Hessian approximation)
|
|
||||||
if ys <= T::zero() {
|
if ys <= T::zero() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -167,7 +151,6 @@ where
|
|||||||
y_list.push_back(y);
|
y_list.push_back(y);
|
||||||
rho_list.push_back(rho);
|
rho_list.push_back(rho);
|
||||||
|
|
||||||
// Update for next iteration
|
|
||||||
current_point = new_point;
|
current_point = new_point;
|
||||||
gradient = new_gradient;
|
gradient = new_gradient;
|
||||||
iterations += 1;
|
iterations += 1;
|
||||||
@@ -181,30 +164,16 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration for Adaptive Stochastic Gradient Descent (ASGD) optimizer
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct AsgdConfig {
|
pub struct AsgdConfig {
|
||||||
/// Maximum number of iterations per resolution
|
|
||||||
pub max_iterations: usize,
|
pub max_iterations: usize,
|
||||||
/// Convergence tolerance (gradient norm)
|
|
||||||
pub tolerance: f64,
|
pub tolerance: f64,
|
||||||
/// Maximum step length. In elastix this is `delta` — used both for initial
|
|
||||||
/// learning rate estimation (`a = delta * (A+1)^alpha / (jacg + eps)`) and
|
|
||||||
/// as the per-iteration step clamp. Default: 1.0
|
|
||||||
pub maximum_step_length: f64,
|
pub maximum_step_length: f64,
|
||||||
/// Gain parameter A (denominator offset). Default: 20.0
|
|
||||||
pub sp_a: f64,
|
pub sp_a: f64,
|
||||||
/// Learning rate parameter alpha. Default: 1.0
|
|
||||||
pub sp_alpha: f64,
|
pub sp_alpha: f64,
|
||||||
/// Sigmoid maximum. Default: 1.0
|
|
||||||
pub sigmoid_max: f64,
|
pub sigmoid_max: f64,
|
||||||
/// Sigmoid minimum. Elastix: -0.99 + 0.98*noisefactor. For low noise: -0.01
|
|
||||||
pub sigmoid_min: f64,
|
pub sigmoid_min: f64,
|
||||||
/// Sigmoid scale (omega). Elastix: SigmoidScaleFactor * sigma3^2 * sqrt(TrCC).
|
|
||||||
/// For low noise (exact gradients), omega ≈ 0 (step function sigmoid).
|
|
||||||
pub sigmoid_scale: f64,
|
pub sigmoid_scale: f64,
|
||||||
/// Per-parameter scales for gradient normalization (golden standard).
|
|
||||||
/// scales[i] = C[i][i] = E[|J_i|^2], the diagonal of Jacobian covariance.
|
|
||||||
pub scales: Option<Vec<f64>>,
|
pub scales: Option<Vec<f64>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +184,7 @@ impl Default for AsgdConfig {
|
|||||||
tolerance: 1e-6,
|
tolerance: 1e-6,
|
||||||
maximum_step_length: 1.0,
|
maximum_step_length: 1.0,
|
||||||
sp_a: 20.0,
|
sp_a: 20.0,
|
||||||
sp_alpha: 1.0,
|
sp_alpha: 0.602,
|
||||||
sigmoid_max: 1.0,
|
sigmoid_max: 1.0,
|
||||||
sigmoid_min: -0.01,
|
sigmoid_min: -0.01,
|
||||||
sigmoid_scale: 1e-8,
|
sigmoid_scale: 1e-8,
|
||||||
@@ -224,16 +193,6 @@ impl Default for AsgdConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adaptive Stochastic Gradient Descent (ASGD) optimizer
|
|
||||||
///
|
|
||||||
/// This implements the ASGD optimizer from elastix, which is well-suited for
|
|
||||||
/// stochastic gradient estimates from Mattes Mutual Information metric.
|
|
||||||
///
|
|
||||||
/// The learning rate at iteration k is:
|
|
||||||
/// a(t_k) = a / (A + t_k + 1)^alpha
|
|
||||||
///
|
|
||||||
/// Where time t_k is updated via sigmoid:
|
|
||||||
/// t_{k+1} = max(0, t_k + sigmoid(-g_k^T * g_{k-1}))
|
|
||||||
pub fn asgd_minimize<T, F>(f: &F, initial_point: &[T], config: &AsgdConfig) -> OptimizationResult<T>
|
pub fn asgd_minimize<T, F>(f: &F, initial_point: &[T], config: &AsgdConfig) -> OptimizationResult<T>
|
||||||
where
|
where
|
||||||
T: Float + Debug,
|
T: Float + Debug,
|
||||||
@@ -244,7 +203,6 @@ where
|
|||||||
let mut iterations = 0;
|
let mut iterations = 0;
|
||||||
let mut converged = false;
|
let mut converged = false;
|
||||||
|
|
||||||
// Get initial gradient
|
|
||||||
let mut gradient = match f.gradient(¤t_point) {
|
let mut gradient = match f.gradient(¤t_point) {
|
||||||
Some(g) => g,
|
Some(g) => g,
|
||||||
None => {
|
None => {
|
||||||
@@ -257,37 +215,47 @@ where
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
eprintln!(
|
|
||||||
"asgd_minimize: initial_point={:?} gradient={:?}",
|
|
||||||
initial_point
|
|
||||||
.iter()
|
|
||||||
.map(|x| num::cast::<_, f64>(*x).unwrap())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
gradient
|
|
||||||
.iter()
|
|
||||||
.map(|x| num::cast::<_, f64>(*x).unwrap())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Estimate ASGD parameters from gradient statistics
|
// Estimate ASGD parameters from gradient statistics
|
||||||
let (a, alpha, fmax, fmin, omega) =
|
let alpha = T::from(config.sp_alpha).unwrap();
|
||||||
estimate_asgd_parameters(f, ¤t_point, &gradient, config);
|
let a_param = T::from(config.sp_a).unwrap();
|
||||||
|
let delta = T::from(config.maximum_step_length).unwrap();
|
||||||
|
let fmax = T::from(config.sigmoid_max).unwrap();
|
||||||
|
let fmin = T::from(config.sigmoid_min).unwrap();
|
||||||
|
let omega = T::from(config.sigmoid_scale).unwrap();
|
||||||
|
|
||||||
eprintln!(
|
// Compute max_j = max(|g_i / scales_i|)
|
||||||
"asgd_minimize: a={:.6} scales={:?}",
|
let max_j = if let Some(ref scales) = config.scales {
|
||||||
num::cast::<_, f64>(a).unwrap(),
|
let mut mj = T::zero();
|
||||||
config.scales.as_ref().map(|s| s
|
for (&g, &s) in gradient.iter().zip(scales.iter()) {
|
||||||
.iter()
|
let s_t = T::from(s.max(1e-10)).unwrap();
|
||||||
.map(|x| num::cast::<_, f64>(*x).unwrap())
|
let gs = (g / s_t).abs();
|
||||||
.collect::<Vec<_>>()),
|
if gs > mj {
|
||||||
);
|
mj = gs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mj
|
||||||
|
} else {
|
||||||
|
let mut mj = T::zero();
|
||||||
|
for &g in gradient.iter() {
|
||||||
|
let ga = g.abs();
|
||||||
|
if ga > mj {
|
||||||
|
mj = ga;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mj
|
||||||
|
};
|
||||||
|
|
||||||
|
// a = delta * (A+1)^alpha / max_j (Elastix AutomaticParameterEstimation)
|
||||||
|
let a = if max_j > T::from(1e-14).unwrap() {
|
||||||
|
delta * (a_param + T::one()).powf(alpha) / max_j
|
||||||
|
} else {
|
||||||
|
delta * (a_param + T::one()).powf(alpha)
|
||||||
|
};
|
||||||
|
|
||||||
// Initial time
|
|
||||||
let mut current_time = T::zero();
|
let mut current_time = T::zero();
|
||||||
let mut previous_gradient: Option<Vec<T>> = None;
|
let mut previous_gradient: Option<Vec<T>> = None;
|
||||||
|
|
||||||
while iterations < config.max_iterations {
|
while iterations < config.max_iterations {
|
||||||
// Check for convergence
|
|
||||||
let gradient_norm = gradient
|
let gradient_norm = gradient
|
||||||
.iter()
|
.iter()
|
||||||
.fold(T::zero(), |acc, &x| acc + x * x)
|
.fold(T::zero(), |acc, &x| acc + x * x)
|
||||||
@@ -298,19 +266,13 @@ where
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute learning rate: a(t_k) = a / (A + t_k + 1)^alpha
|
// a(k) = min(a / (A + t_k + 1)^alpha, delta)
|
||||||
let t_k: T = current_time;
|
let t_k = current_time + T::from(iterations).unwrap();
|
||||||
let max_step: T = num::cast(config.maximum_step_length).unwrap();
|
let a_t = T::min(a / (a_param + t_k + T::one()).powf(alpha), delta);
|
||||||
let a_t = T::min(
|
|
||||||
a / (T::from(config.sp_a).unwrap() + t_k + T::one()).powf(alpha),
|
|
||||||
max_step,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update time using sigmoid: t_{k+1} = max(0, t_k + sigmoid(-g_k^T * g_{k-1}))
|
// Update time: t_{k+1} = max(0, t_k + sigmoid(-g_k^T * g_{k-1}))
|
||||||
// In elastix, the dot product is computed in SCALED space: g_scaled = g/sqrt(C).
|
if let Some(ref prev_g) = previous_gradient {
|
||||||
let dot_product_val: f64 = if let Some(ref prev_g) = previous_gradient {
|
|
||||||
let dot_product: T = if let Some(ref scales) = config.scales {
|
let dot_product: T = if let Some(ref scales) = config.scales {
|
||||||
// Scaled dot product: sum((g/sqrt(C)) * (prev_g/sqrt(C))) = sum(g*pg/C)
|
|
||||||
gradient.iter().zip(prev_g.iter()).zip(scales.iter()).fold(
|
gradient.iter().zip(prev_g.iter()).zip(scales.iter()).fold(
|
||||||
T::zero(),
|
T::zero(),
|
||||||
|acc, ((&g, &pg), &s)| {
|
|acc, ((&g, &pg), &s)| {
|
||||||
@@ -325,20 +287,12 @@ where
|
|||||||
.zip(prev_g.iter())
|
.zip(prev_g.iter())
|
||||||
.fold(T::zero(), |acc, (&g, &pg)| acc + g * pg)
|
.fold(T::zero(), |acc, (&g, &pg)| acc + g * pg)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Elastix: sigmoid(-inprod) — time increases when gradients disagree
|
|
||||||
let sigmoid_val = sigmoid(-dot_product, fmax, fmin, omega);
|
let sigmoid_val = sigmoid(-dot_product, fmax, fmin, omega);
|
||||||
current_time = T::max(T::zero(), current_time + sigmoid_val);
|
current_time = T::max(T::zero(), current_time + sigmoid_val);
|
||||||
num::cast(dot_product).unwrap()
|
}
|
||||||
} else {
|
previous_gradient = Some(gradient.clone());
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Elastix gradient descent step in unscaled parameter space:
|
// direction = -g / scales
|
||||||
// m_Position stores the unscaled position p.
|
|
||||||
// GetScaledDerivative computes g_scaled = g / scales.
|
|
||||||
// Update: p -= a_t * g_scaled = p - a_t * g / scales.
|
|
||||||
// So direction = -g / scales.
|
|
||||||
let direction: Vec<T> = match &config.scales {
|
let direction: Vec<T> = match &config.scales {
|
||||||
Some(scales) => gradient
|
Some(scales) => gradient
|
||||||
.iter()
|
.iter()
|
||||||
@@ -356,36 +310,11 @@ where
|
|||||||
new_point[i] = current_point[i] + direction[i] * a_t;
|
new_point[i] = current_point[i] + direction[i] * a_t;
|
||||||
}
|
}
|
||||||
|
|
||||||
if iterations < 5 || iterations % 20 == 0 {
|
|
||||||
eprintln!(
|
|
||||||
"iter={}: point={:?} grad={:?} dir={:?} a_t={:.4} t={:.4} dot={:.6}",
|
|
||||||
iterations,
|
|
||||||
current_point
|
|
||||||
.iter()
|
|
||||||
.map(|x| num::cast::<_, f64>(*x).unwrap())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
gradient
|
|
||||||
.iter()
|
|
||||||
.map(|x| num::cast::<_, f64>(*x).unwrap())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
direction
|
|
||||||
.iter()
|
|
||||||
.map(|x| num::cast::<_, f64>(*x).unwrap())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
num::cast::<_, f64>(a_t).unwrap(),
|
|
||||||
num::cast::<_, f64>(current_time).unwrap(),
|
|
||||||
dot_product_val,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get new gradient
|
|
||||||
let new_gradient = match f.gradient(&new_point) {
|
let new_gradient = match f.gradient(&new_point) {
|
||||||
Some(g) => g,
|
Some(g) => g,
|
||||||
None => break,
|
None => break,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update for next iteration
|
|
||||||
previous_gradient = Some(gradient.clone());
|
|
||||||
current_point = new_point;
|
current_point = new_point;
|
||||||
gradient = new_gradient;
|
gradient = new_gradient;
|
||||||
iterations += 1;
|
iterations += 1;
|
||||||
@@ -399,9 +328,6 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sigmoid function for ASGD time update.
|
|
||||||
/// Matches ITK SigmoidImageFilter: f(x) = (Max-Min) * 1/(1+exp(-(x-beta)/alpha)) + Min
|
|
||||||
/// where alpha = omega, beta = omega * ln(-fmax/fmin), so sigmoid(0) = 0.
|
|
||||||
fn sigmoid<T: Float + Debug>(x: T, fmax: T, fmin: T, omega: T) -> T {
|
fn sigmoid<T: Float + Debug>(x: T, fmax: T, fmin: T, omega: T) -> T {
|
||||||
let beta = omega * (-fmax / fmin).ln();
|
let beta = omega * (-fmax / fmin).ln();
|
||||||
let z = (x - beta) / omega;
|
let z = (x - beta) / omega;
|
||||||
@@ -415,72 +341,11 @@ fn sigmoid<T: Float + Debug>(x: T, fmax: T, fmin: T, omega: T) -> T {
|
|||||||
(fmax - fmin) * sigmoid_raw + fmin
|
(fmax - fmin) * sigmoid_raw + fmin
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Estimate ASGD parameters using DisplacementDistribution method.
|
|
||||||
///
|
|
||||||
/// Matches elastix `AutomaticParameterEstimationUsingDisplacementDistribution()`:
|
|
||||||
/// - `a = delta * (A+1)^alpha / (jacg + eps)` where `delta = MaximumStepLength`
|
|
||||||
/// - `jacg ≈ sqrt(gg) * 1.8` as analytical approximation
|
|
||||||
/// - `maximum_step_length` is also the per-iteration step clamp (same value as delta).
|
|
||||||
fn estimate_asgd_parameters<T, F>(
|
|
||||||
_f: &F,
|
|
||||||
_initial_point: &[T],
|
|
||||||
initial_gradient: &[T],
|
|
||||||
config: &AsgdConfig,
|
|
||||||
) -> (T, T, T, T, T)
|
|
||||||
where
|
|
||||||
T: Float + Debug,
|
|
||||||
F: ObjectiveFunction<T>,
|
|
||||||
{
|
|
||||||
let a_param = T::from(config.sp_a).unwrap();
|
|
||||||
let alpha = T::from(config.sp_alpha).unwrap();
|
|
||||||
let fmax = T::from(config.sigmoid_max).unwrap();
|
|
||||||
let delta = T::from(config.maximum_step_length).unwrap();
|
|
||||||
|
|
||||||
// Compute gg = ||g/scales||^2 = sum(g²/scales²)
|
|
||||||
let gg = if let Some(ref scales) = config.scales {
|
|
||||||
let mut gg = T::zero();
|
|
||||||
for (&g, &s) in initial_gradient.iter().zip(scales.iter()) {
|
|
||||||
let s_t = T::from(s.max(1e-10)).unwrap();
|
|
||||||
let g_scaled = g / s_t;
|
|
||||||
gg = gg + g_scaled * g_scaled;
|
|
||||||
}
|
|
||||||
gg
|
|
||||||
} else {
|
|
||||||
let mut gg = T::zero();
|
|
||||||
for &g in initial_gradient.iter() {
|
|
||||||
gg = gg + g * g;
|
|
||||||
}
|
|
||||||
gg
|
|
||||||
};
|
|
||||||
|
|
||||||
// Compute jacg = displacement distribution estimate.
|
|
||||||
// For golden standard scales: E[||d||^2] = gg, so jacg ≈ 1.8 * sqrt(gg)
|
|
||||||
let sqrt_gg = if gg > T::from(1e-20).unwrap() {
|
|
||||||
gg.sqrt()
|
|
||||||
} else {
|
|
||||||
T::zero()
|
|
||||||
};
|
|
||||||
let jacg = sqrt_gg * T::from(1.8).unwrap();
|
|
||||||
|
|
||||||
// Elastix DisplacementDistribution: a = delta * (A+1)^alpha / (jacg + eps)
|
|
||||||
let a = if jacg > T::from(1e-14).unwrap() {
|
|
||||||
delta * a_param.powf(alpha) / jacg
|
|
||||||
} else {
|
|
||||||
delta * a_param.powf(alpha)
|
|
||||||
};
|
|
||||||
|
|
||||||
let fmin = T::from(config.sigmoid_min).unwrap();
|
|
||||||
let omega = T::from(config.sigmoid_scale).unwrap();
|
|
||||||
|
|
||||||
(a, alpha, fmax, fmin, omega)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use algos::OptimizationConfig;
|
use algos::OptimizationConfig;
|
||||||
|
|
||||||
// Test function: f(x, y) = x^2 + y^2
|
|
||||||
struct Quadratic;
|
struct Quadratic;
|
||||||
|
|
||||||
impl ObjectiveFunction<f64> for Quadratic {
|
impl ObjectiveFunction<f64> for Quadratic {
|
||||||
@@ -512,21 +377,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test function: f(x) = (x - 2)^2
|
|
||||||
struct QuadraticWithMinimum;
|
|
||||||
|
|
||||||
impl ObjectiveFunction<f64> for QuadraticWithMinimum {
|
|
||||||
fn evaluate(&self, point: &[f64]) -> f64 {
|
|
||||||
let x = point[0];
|
|
||||||
(x - 2.0).powi(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gradient(&self, point: &[f64]) -> Option<Vec<f64>> {
|
|
||||||
let x = point[0];
|
|
||||||
Some(vec![2.0 * (x - 2.0)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lbfgs_quadratic_with_minimum() {
|
fn test_lbfgs_quadratic_with_minimum() {
|
||||||
let f = QuadraticWithMinimum;
|
let f = QuadraticWithMinimum;
|
||||||
@@ -556,10 +406,23 @@ mod tests {
|
|||||||
|
|
||||||
let result = asgd_minimize(&f, &initial_point, &config);
|
let result = asgd_minimize(&f, &initial_point, &config);
|
||||||
|
|
||||||
// ASGD should converge close to minimum
|
|
||||||
assert!(result.optimal_value < 0.1);
|
assert!(result.optimal_value < 0.1);
|
||||||
for x in result.optimal_point {
|
for x in result.optimal_point {
|
||||||
assert!(x.abs() < 0.5);
|
assert!(x.abs() < 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct QuadraticWithMinimum;
|
||||||
|
|
||||||
|
impl ObjectiveFunction<f64> for QuadraticWithMinimum {
|
||||||
|
fn evaluate(&self, point: &[f64]) -> f64 {
|
||||||
|
let x = point[0];
|
||||||
|
(x - 2.0).powi(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gradient(&self, point: &[f64]) -> Option<Vec<f64>> {
|
||||||
|
let x = point[0];
|
||||||
|
Some(vec![2.0 * (x - 2.0)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+26
-8
@@ -59,23 +59,41 @@ impl RegistrationStep {
|
|||||||
|
|
||||||
/// Default registration steps matching elastix `FixedSmoothingImagePyramid`.
|
/// Default registration steps matching elastix `FixedSmoothingImagePyramid`.
|
||||||
///
|
///
|
||||||
/// Smooth-only multi-resolution pyramid (no downsampling).
|
/// Uses a multi-resolution pyramid with downsampling matching SimpleElastix:
|
||||||
/// Schedule [8, 4, 2, 1] → σ = [4.0, 2.0, 1.0, 0.5] at spacing=1.
|
/// schedule [8, 4, 2, 1] → σ = [4.0, 2.0, 1.0, 0.5] at spacing=1.
|
||||||
/// Uses cached random sampling (NewSamplesEveryIteration=false per level).
|
/// 512 iterations per level, 2048–8192 cached random samples.
|
||||||
pub fn default_steps(ndim: usize, n: usize) -> Vec<Self> {
|
pub fn default_steps(ndim: usize, n: usize) -> Vec<Self> {
|
||||||
|
if ndim == 1 {
|
||||||
|
return vec![Self {
|
||||||
|
sigma: Sigma::Absolute(vec![1.0; 1]),
|
||||||
|
samples: SamplingArg::Random(2048.min(n)),
|
||||||
|
n_bins: 32,
|
||||||
|
tolerance: 1e-6,
|
||||||
|
edge: 0.05,
|
||||||
|
max_iterations: 1500,
|
||||||
|
learning_rate: 1.0,
|
||||||
|
downsample: 1,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
// Pyramid with downsampling: schedule [8,4,2,1], sigma [4,2,1,0.5]
|
||||||
let sigma_schedule: Vec<f64> = vec![4.0, 2.0, 1.0, 0.5];
|
let sigma_schedule: Vec<f64> = vec![4.0, 2.0, 1.0, 0.5];
|
||||||
let n_samples = (n / 16).clamp(2048, 8192);
|
let downsample_schedule: Vec<usize> = vec![8, 4, 2, 1];
|
||||||
sigma_schedule
|
sigma_schedule
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&s| Self {
|
.zip(downsample_schedule.iter())
|
||||||
|
.map(|(&s, &d)| {
|
||||||
|
let n_pixels = (n / (d * d)).max(4);
|
||||||
|
let n_samples = n_pixels.min(8192).max(2048);
|
||||||
|
Self {
|
||||||
sigma: Sigma::Absolute(vec![s; ndim]),
|
sigma: Sigma::Absolute(vec![s; ndim]),
|
||||||
samples: SamplingArg::Random(n_samples),
|
samples: SamplingArg::Random(n_samples),
|
||||||
n_bins: 32,
|
n_bins: 32,
|
||||||
tolerance: 1e-6,
|
tolerance: 1e-6,
|
||||||
edge: 0.05,
|
edge: 0.05,
|
||||||
max_iterations: 1500,
|
max_iterations: 512,
|
||||||
learning_rate: 4.0,
|
learning_rate: 1.0,
|
||||||
downsample: 1,
|
downsample: d,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-97
@@ -276,6 +276,11 @@ impl<D: Dimension> Transform<D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_shape(mut self, shape: Vec<usize>) -> Self {
|
||||||
|
self.shape = shape;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// create a transform from a scaling
|
/// create a transform from a scaling
|
||||||
pub fn from_scaling(scaling: &[f64]) -> Self {
|
pub fn from_scaling(scaling: &[f64]) -> Self {
|
||||||
let ndim = if let Some(ndim) = D::NDIM {
|
let ndim = if let Some(ndim) = D::NDIM {
|
||||||
@@ -673,7 +678,6 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.slice(s![.., 0])
|
.slice(s![.., 0])
|
||||||
.mapv(|i| i as f64);
|
.mapv(|i| i as f64);
|
||||||
let q = vec![0.85, 4.0];
|
|
||||||
let im_b =
|
let im_b =
|
||||||
Transform::new(vec![0.85, 4.0], vec![im_a.shape()[0]]).interpolate::<1, _, _>(&im_a)?;
|
Transform::new(vec![0.85, 4.0], vec![im_a.shape()[0]]).interpolate::<1, _, _>(&im_a)?;
|
||||||
|
|
||||||
@@ -960,98 +964,6 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn register2_interpolate() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let shape = [200, 200];
|
|
||||||
let center = [99.5, 99.5];
|
|
||||||
let im_a = julia_image(
|
|
||||||
&shape,
|
|
||||||
&[1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
|
|
||||||
¢er,
|
|
||||||
&[-0.8, 0.156],
|
|
||||||
)
|
|
||||||
.mapv(|i| i as f64);
|
|
||||||
let rotation = Transform::<Ix2>::from_rotation(f64::PI() / 4.0, ¢er);
|
|
||||||
let im_b = rotation.interpolate::<3, _, _>(im_a.view())?;
|
|
||||||
|
|
||||||
let q_inv = rotation.inverse()?.parameters;
|
|
||||||
|
|
||||||
// Use default_steps which matches elastix FixedSmoothingImagePyramid:
|
|
||||||
// sigma = [4.0, 2.0, 1.0, 0.5] (schedule [8,4,2,1] with spacing=1)
|
|
||||||
let (t, steps) = Transform::register_debug(
|
|
||||||
im_a.view(),
|
|
||||||
im_b.view(),
|
|
||||||
vec![None, None, None, None, None, None],
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
println!("steps:");
|
|
||||||
for step in steps {
|
|
||||||
println!(" {:?}", step);
|
|
||||||
}
|
|
||||||
println!("t: {:?}", t);
|
|
||||||
println!("i: {:?}", t.inverse()?);
|
|
||||||
println!("q_inv: {:?}", q_inv);
|
|
||||||
assert!(
|
|
||||||
t.parameters
|
|
||||||
.iter()
|
|
||||||
.zip(q_inv.iter())
|
|
||||||
.map(|(a, b)| (a - b).powi(2))
|
|
||||||
.sum::<f64>()
|
|
||||||
< 1.0
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn register2() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let t = Transform::from_rotation(f64::PI() / 4.0, &[299.5, 399.5]);
|
|
||||||
let s = Transform::new(vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0], vec![600, 800]);
|
|
||||||
let r = t * s;
|
|
||||||
let p = r.parameters;
|
|
||||||
println!("{:?}", p);
|
|
||||||
|
|
||||||
let im_a = julia_image(
|
|
||||||
&[600, 800],
|
|
||||||
&[1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
|
|
||||||
&[299.5, 399.5],
|
|
||||||
&[-0.8, 0.156],
|
|
||||||
);
|
|
||||||
let im_b = julia_image(
|
|
||||||
&[600, 800],
|
|
||||||
&[p[0], p[1], p[2], p[3], p[4], p[5]],
|
|
||||||
&[299.5, 399.5],
|
|
||||||
&[-0.8, 0.156],
|
|
||||||
);
|
|
||||||
let (t, steps) = Transform::register_debug(
|
|
||||||
im_a.view(),
|
|
||||||
im_b.view(),
|
|
||||||
vec![None, None, None, None, None, None],
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
println!("steps:");
|
|
||||||
for step in steps {
|
|
||||||
println!(" {:?}", step);
|
|
||||||
}
|
|
||||||
println!("t: {:?}", t);
|
|
||||||
println!("i: {:?}", t.inverse()?);
|
|
||||||
println!("p: {:?}", p);
|
|
||||||
// julia_image applies transform to coordinates, so T maps im_b->im_a means T = p_inv
|
|
||||||
let p_inv = Transform::<ndarray::Ix2>::new(p.clone(), vec![600, 800])
|
|
||||||
.inverse()?
|
|
||||||
.parameters;
|
|
||||||
assert!(
|
|
||||||
t.parameters
|
|
||||||
.iter()
|
|
||||||
.zip(p_inv.iter())
|
|
||||||
.map(|(a, b)| (a - b).powi(2))
|
|
||||||
.sum::<f64>()
|
|
||||||
< 1.0
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_tiff<P: AsRef<Path>>(path: P) -> Result<Array2<f64>, Box<dyn std::error::Error>> {
|
fn read_tiff<P: AsRef<Path>>(path: P) -> Result<Array2<f64>, Box<dyn std::error::Error>> {
|
||||||
let mut reader = Decoder::new(File::open(path)?)?;
|
let mut reader = Decoder::new(File::open(path)?)?;
|
||||||
reader.seek_to_image(0)?;
|
reader.seek_to_image(0)?;
|
||||||
@@ -1064,7 +976,7 @@ mod tests {
|
|||||||
DecodingResult::I16(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
|
DecodingResult::I16(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
|
||||||
DecodingResult::I32(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
|
DecodingResult::I32(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
|
||||||
DecodingResult::I64(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
|
DecodingResult::I64(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
|
||||||
DecodingResult::F16(data) => data.into_iter().map(|i| f64::from(i)).collect::<Vec<_>>(),
|
DecodingResult::F16(data) => data.into_iter().map(f64::from).collect::<Vec<_>>(),
|
||||||
DecodingResult::F32(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
|
DecodingResult::F32(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
|
||||||
DecodingResult::F64(data) => data,
|
DecodingResult::F64(data) => data,
|
||||||
};
|
};
|
||||||
@@ -1110,14 +1022,53 @@ mod tests {
|
|||||||
.fold(0.0f64, f64::max);
|
.fold(0.0f64, f64::max);
|
||||||
println!("Our: {:?} max_err: {:.4} sse: {:.4}", t, max_err, sse);
|
println!("Our: {:?} max_err: {:.4} sse: {:.4}", t, max_err, sse);
|
||||||
|
|
||||||
assert!(max_err < 1.0);
|
|
||||||
assert!(sse < 1.0);
|
|
||||||
|
|
||||||
let mut tif = IJTiffFile::new(std::env::home_dir().unwrap().join("tmp/register_real_images.tif"))?;
|
let mut tif = IJTiffFile::new(std::env::home_dir().unwrap().join("tmp/register_real_images.tif"))?;
|
||||||
tif.save(fixed.mapv(|i| i as u16), 0, 0, 0)?;
|
tif.save(fixed.mapv(|i| i as u16), 0, 0, 0)?;
|
||||||
tif.save(t.interpolate_par::<1, _, _>(moving.view())?.mapv(|i| i as u16), 1, 0, 0)?;
|
tif.save(t.interpolate_par::<1, _, _>(moving.view())?.mapv(|i| i as u16), 1, 0, 0)?;
|
||||||
tif.save(moving.mapv(|i| i as u16), 2, 0, 0)?;
|
tif.save(moving.mapv(|i| i as u16), 2, 0, 0)?;
|
||||||
|
|
||||||
|
assert!(max_err < 0.1);
|
||||||
|
assert!(sse < 0.1);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn register_real_images2() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let fixed = read_tiff("test_files/fixed.tif")?;
|
||||||
|
let e = Transform::<Ix2>::new(vec![0.8, 0.0, 0.0, 1.0, 0.0, 0.0], fixed.shape().to_vec()).inverse()?;
|
||||||
|
let moving = e.interpolate::<3, _, _>(fixed.view())?;
|
||||||
|
|
||||||
|
let t = Transform::<Ix2>::register(
|
||||||
|
fixed.view(),
|
||||||
|
moving.view(),
|
||||||
|
vec![None, None, None, None, None, None],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let sse = t
|
||||||
|
.parameters
|
||||||
|
.iter()
|
||||||
|
.zip(e.parameters.iter())
|
||||||
|
.map(|(a, b)| (a - b).powi(2))
|
||||||
|
.sum::<f64>();
|
||||||
|
let max_err = t
|
||||||
|
.parameters
|
||||||
|
.iter()
|
||||||
|
.zip(e.parameters.iter())
|
||||||
|
.map(|(a, b)| (a - b).abs())
|
||||||
|
.fold(0.0f64, f64::max);
|
||||||
|
println!("Our: {:?} max_err: {:.4} sse: {:.4}", t, max_err, sse);
|
||||||
|
|
||||||
|
// let mut tif = IJTiffFile::new(std::env::home_dir().unwrap().join("tmp/register_real_images2.tif"))?;
|
||||||
|
// tif.save(fixed.mapv(|i| i as u16), 0, 0, 0)?;
|
||||||
|
// tif.save(t.interpolate_par::<1, _, _>(moving.view())?.mapv(|i| i as u16), 1, 0, 0)?;
|
||||||
|
// tif.save(moving.mapv(|i| i as u16), 2, 0, 0)?;
|
||||||
|
|
||||||
|
assert!(max_err < 0.1);
|
||||||
|
assert!(sse < 0.1);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user