use ndarray::{Array1, Array2};
use rgmin::vecops::{axpy, dot, nrm2};
use rgmin::{Manifold, prfo_restricted, rfo_get_s};
const TRUST_ITERS: usize = 64;
pub fn prfo_trust_region(
evals: &Array1<f64>,
evecs: &Array2<f64>,
g: &Array1<f64>,
order: usize,
delta: f64,
) -> Array1<f64> {
let stepper = PartitionedRationalFunctionOptimization::new(order);
let s1 = stepper.get_s(
evals,
evecs,
g,
PartitionedRationalFunctionOptimization::ALPHA0,
);
let n1 = nrm2(s1.view());
if n1 <= delta + 1e-14 {
return s1;
}
let mut lo = 0.0;
let mut hi = 1.0;
let mut best = s1;
for _ in 0..TRUST_ITERS {
let mid = 0.5 * (lo + hi);
let s = stepper.get_s(evals, evecs, g, mid);
let val = nrm2(s.view());
best = s;
if (val - delta).abs() <= 1e-10 {
return best;
}
if val > delta {
hi = mid;
} else {
lo = mid;
}
}
best
}
use crate::rfo::RationalFunctionOptimization;
pub const SYNONYMS: &[&str] = &[
"prfo",
"p-rfo",
"partitioned rational function optimization",
];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct PartitionedRationalFunctionOptimization {
pub order: usize,
}
impl PartitionedRationalFunctionOptimization {
pub const ALPHA0: f64 = RationalFunctionOptimization::ALPHA0;
pub const ALPHAMIN: f64 = RationalFunctionOptimization::ALPHAMIN;
pub const ALPHAMAX: f64 = RationalFunctionOptimization::ALPHAMAX;
pub const SLOPE: f64 = RationalFunctionOptimization::SLOPE;
pub const NEWTON_SAFE: bool = RationalFunctionOptimization::NEWTON_SAFE;
pub fn new(order: usize) -> Self {
Self { order }
}
pub fn match_name(name: &str) -> bool {
SYNONYMS.contains(&name)
}
pub fn from_name(name: &str, order: usize) -> Option<Self> {
Self::match_name(name).then_some(Self { order })
}
pub fn get_s(
&self,
evals: &Array1<f64>,
evecs: &Array2<f64>,
g: &Array1<f64>,
alpha: f64,
) -> Array1<f64> {
let n = g.len();
if n == 0 || evals.len() != n || evecs.nrows() != n || evecs.ncols() != n {
return Array1::zeros(n);
}
let order = self.order.min(n);
let mut s = Array1::zeros(n);
if order > 0 {
let mut gmax = Array1::zeros(order);
let mut hmax = Array2::<f64>::zeros((order, order));
for i in 0..order {
hmax[(i, i)] = evals[i];
gmax[i] = dot(evecs.column(i), g.view());
}
let smax = rfo_get_s(&hmax, &gmax, order, alpha);
for i in 0..order {
axpy(smax[i], evecs.column(i), &mut s);
}
}
let nmin = n - order;
if nmin > 0 {
let mut gmin = Array1::zeros(nmin);
let mut hmin = Array2::<f64>::zeros((nmin, nmin));
for i in 0..nmin {
hmin[(i, i)] = evals[order + i];
gmin[i] = dot(evecs.column(order + i), g.view());
}
let smin = rfo_get_s(&hmin, &gmin, 0, alpha);
for i in 0..nmin {
axpy(smin[i], evecs.column(order + i), &mut s);
}
}
s
}
pub fn restricted(
&self,
evals: &Array1<f64>,
evecs: &Array2<f64>,
g: &Array1<f64>,
delta: f64,
) -> Array1<f64> {
prfo_restricted(evals, evecs, g, self.order, delta)
}
pub fn trust_region(
&self,
evals: &Array1<f64>,
evecs: &Array2<f64>,
g: &Array1<f64>,
delta: f64,
) -> Array1<f64> {
prfo_trust_region(evals, evecs, g, self.order, delta)
}
pub fn step_on<M: Manifold>(
&self,
man: &M,
x: &Array1<f64>,
evals: &Array1<f64>,
evecs: &Array2<f64>,
g: &Array1<f64>,
alpha: f64,
) -> Array1<f64> {
let g_r = man.egrad2rgrad(x, g);
let s = self.get_s(evals, evecs, &g_r, alpha);
let v = man.project(x, &s);
man.retract(x, &v)
}
#[expect(
clippy::too_many_arguments,
reason = "The public transport API names the two points and the spectral step inputs."
)]
pub fn transport_step<M: Manifold>(
&self,
man: &M,
x: &Array1<f64>,
x_to: &Array1<f64>,
evals: &Array1<f64>,
evecs: &Array2<f64>,
g: &Array1<f64>,
alpha: f64,
) -> Array1<f64> {
let g_r = man.egrad2rgrad(x, g);
let s = self.get_s(evals, evecs, &g_r, alpha);
let v = man.project(x, &s);
man.transport(x, x_to, &v)
}
}
pub fn prfo_stepper(name: &str, order: usize) -> Option<PartitionedRationalFunctionOptimization> {
PartitionedRationalFunctionOptimization::from_name(name, order)
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::{Array2, array};
use rgmin::ManifoldKind;
use rgmin::rfo_get_s;
use rgmin::vecops::{dot, nrm2};
#[test]
fn factory_matches_sella_synonyms() {
assert!(PartitionedRationalFunctionOptimization::match_name("prfo"));
assert!(PartitionedRationalFunctionOptimization::match_name("p-rfo"));
assert!(PartitionedRationalFunctionOptimization::match_name(
"partitioned rational function optimization"
));
assert!(prfo_stepper("prfo", 1).is_some());
assert!(prfo_stepper("rfo", 1).is_none());
assert!(prfo_stepper("qn", 0).is_none());
}
#[test]
fn contract_bounds_match_sella_rfo() {
assert_eq!(
PartitionedRationalFunctionOptimization::ALPHA0,
RationalFunctionOptimization::ALPHA0
);
assert_eq!(PartitionedRationalFunctionOptimization::ALPHA0, 1.0);
assert_eq!(PartitionedRationalFunctionOptimization::ALPHAMIN, 0.0);
assert_eq!(PartitionedRationalFunctionOptimization::ALPHAMAX, 1.0);
assert_eq!(PartitionedRationalFunctionOptimization::SLOPE, 1.0);
assert!(!PartitionedRationalFunctionOptimization::NEWTON_SAFE);
}
#[test]
fn order_zero_matches_rfo_on_the_full_space() {
let evals = array![2.0, 5.0];
let evecs = Array2::<f64>::eye(2);
let g = array![2.0, 0.4];
let h = Array2::from_diag(&evals);
let s_prfo = PartitionedRationalFunctionOptimization::new(0).get_s(&evals, &evecs, &g, 1.0);
let s_rfo = rfo_get_s(&h, &g, 0, 1.0);
let diff = nrm2((&s_prfo - &s_rfo).view());
assert!(
diff < 1e-10,
"order-0 P-RFO != RFO: {s_prfo:?} vs {s_rfo:?}"
);
}
#[test]
fn order_one_moves_along_the_soft_mode() {
let evals = array![-1.0, 4.0];
let evecs = Array2::<f64>::eye(2);
let g = array![0.5, 0.4];
let s = PartitionedRationalFunctionOptimization::new(1).get_s(&evals, &evecs, &g, 1.0);
assert!(s[0].abs() > 1e-8, "no uphill component: {s:?}");
assert!(s.iter().all(|v| v.is_finite()));
}
#[test]
fn order_partitions_differ_from_full_space_rfo() {
let evals = array![-1.0, 4.0];
let evecs = Array2::<f64>::eye(2);
let g = array![0.5, 0.4];
let h = Array2::from_diag(&evals);
let s_prfo = PartitionedRationalFunctionOptimization::new(1).get_s(&evals, &evecs, &g, 1.0);
let s_rfo = rfo_get_s(&h, &g, 1, 1.0);
let diff = nrm2((&s_prfo - &s_rfo).view());
assert!(diff > 1e-8, "P-RFO collapsed to RFO: {s_prfo:?} {s_rfo:?}");
}
#[test]
fn step_grows_with_alpha() {
let evals = array![-1.0, 4.0];
let evecs = Array2::<f64>::eye(2);
let g = array![0.5, 0.4];
let stepper = PartitionedRationalFunctionOptimization::new(1);
let n_lo = nrm2(stepper.get_s(&evals, &evecs, &g, 0.2).view());
let n_hi = nrm2(stepper.get_s(&evals, &evecs, &g, 0.8).view());
assert!(n_hi > n_lo, "slope flipped: {n_lo} vs {n_hi}");
let n0 = nrm2(stepper.get_s(&evals, &evecs, &g, 0.0).view());
assert!(n0 < 1e-12, "alpha=0 must vanish: {n0}");
}
#[test]
fn prfo_step_on_the_sphere_stays_on_the_set() {
let man = ManifoldKind::Sphere;
let x = array![0.0, 1.0, 0.0];
let evals = array![-1.0, 2.0, 3.0];
let evecs = Array2::<f64>::eye(3);
let g = array![1.0, 0.2, -0.3];
let stepper = PartitionedRationalFunctionOptimization::new(1);
let y = stepper.step_on(&man, &x, &evals, &evecs, &g, 1.0);
let n = nrm2(y.view());
assert!((n - 1.0).abs() < 1e-12, "||y||={n} y={y:?}");
assert!(y.iter().all(|v| v.is_finite()));
let g_r = man.egrad2rgrad(&x, &g);
let s = stepper.get_s(&evals, &evecs, &g_r, 1.0);
let v = man.project(&x, &s);
assert!(
dot(x.view(), v.view()).abs() < 1e-12,
"step is not tangent: x·v={}",
dot(x.view(), v.view())
);
let w = stepper.transport_step(&man, &x, &y, &evals, &evecs, &g, 1.0);
assert!(
dot(y.view(), w.view()).abs() < 1e-12,
"transported step leaves T_y: y·w={}",
dot(y.view(), w.view())
);
}
#[test]
fn prfo_step_on_rigid_quotient_is_horizontal() {
let man = ManifoldKind::RigidQuotient;
let x = array![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
let evals = Array1::from(vec![-1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 5.0]);
let evecs = Array2::<f64>::eye(9);
let g = array![0.4, 0.1, 0.0, 0.2, -0.3, 0.0, -0.1, 0.05, 0.0];
let stepper = PartitionedRationalFunctionOptimization::new(1);
let y = stepper.step_on(&man, &x, &evals, &evecs, &g, 1.0);
assert_eq!(y.len(), 9);
assert!(y.iter().all(|v| v.is_finite()));
let dx = &y - &x;
let horiz = man.project(&x, &dx);
let leak = nrm2((&dx - &horiz).view());
assert!(
leak < 1e-12,
"retracted increment left the horizontal: {leak}"
);
}
#[test]
fn restricted_clips_a_long_prfo_step() {
let evals = array![-1.0, 4.0];
let evecs = Array2::<f64>::eye(2);
let g = array![8.0, 4.0];
let stepper = PartitionedRationalFunctionOptimization::new(1);
let full = nrm2(stepper.get_s(&evals, &evecs, &g, 1.0).view());
let delta = 0.05;
assert!(full > delta, "unrestricted ||s||={full}");
let s = stepper.restricted(&evals, &evecs, &g, delta);
let n = nrm2(s.view());
assert!(n <= delta + 1e-10, "||s||={n} delta={delta} full={full}");
assert!(s.iter().all(|v| v.is_finite()));
}
}