From ba28379fbe146be09175ec7aa1365c0169720317 Mon Sep 17 00:00:00 2001 From: Yuwen Chen Date: Mon, 7 Aug 2023 10:16:46 +0100 Subject: [PATCH 01/52] add generalized power cones, but haven't debugged it yet --- src/solver/core/cones/exppow_common.rs | 77 +++ src/solver/core/cones/genpowcone.rs | 476 ++++++++++++++++++ src/solver/core/cones/mod.rs | 3 +- src/solver/core/cones/supportedcone.rs | 11 + .../kktsolvers/direct/quasidef/datamap.rs | 30 +- .../direct/quasidef/directldlkktsolver.rs | 77 ++- .../core/kktsolvers/direct/quasidef/utils.rs | 103 +++- 7 files changed, 745 insertions(+), 32 deletions(-) create mode 100644 src/solver/core/cones/genpowcone.rs diff --git a/src/solver/core/cones/exppow_common.rs b/src/solver/core/cones/exppow_common.rs index 5ae6689b..e3908d6b 100644 --- a/src/solver/core/cones/exppow_common.rs +++ b/src/solver/core/cones/exppow_common.rs @@ -190,3 +190,80 @@ where α } } + + + +// -------------------------------------- +// Traits and blanket implementations for Generalized PowerCones +// ------------------------------------- + +// Operations supported on 3d nonsymmetrics only +pub(crate) trait NonsymmetricCone { + // Returns true if s is primal feasible + fn is_primal_feasible(& self, s: &[T]) -> bool; + + // Returns true if z is dual feasible + fn is_dual_feasible(& self, z: &[T]) -> bool; + + // Compute the primal gradient of f(s) at s + fn minus_gradient_primal(& self, s: &[T]) -> (T,T); + + fn update_dual_grad_H(&mut self, z: &[T]); + + fn barrier_dual(&self, z: &[T]) -> T; + + fn barrier_primal(&self, s: &[T]) -> T; +} + +#[allow(clippy::too_many_arguments)] +pub(crate) trait NonsymmetricConeUtils { + fn step_length_n_cone( + & self, + wq: &mut [T], + dq: &[T], + q: &[T], + α_init: T, + α_min: T, + backtrack: T, + is_in_cone_fcn: impl Fn(&[T]) -> bool, + ) -> T; +} + +impl NonsymmetricConeUtils for C +where + T: FloatT, + C: NonsymmetricCone, +{ + // find the maximum step length α≥0 so that + // q + α*dq stays in an exponential or power + // cone, or their respective dual cones. + fn step_length_n_cone( + & self, + wq: &mut [T], + dq: &[T], + q: &[T], + α_init: T, + α_min: T, + backtrack: T, + is_in_cone_fcn: impl Fn(&[T]) -> bool, + ) -> T { + let mut α = α_init; + + loop { + // wq = q + α*dq + for i in 0..q.len() { + wq[i] = q[i] + α * dq[i]; + } + + if is_in_cone_fcn(wq) { + break; + } + α *= backtrack; + if α < α_min { + α = T::zero(); + break; + } + } + α + } +} \ No newline at end of file diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs new file mode 100644 index 00000000..22fd8349 --- /dev/null +++ b/src/solver/core/cones/genpowcone.rs @@ -0,0 +1,476 @@ +use super::*; +use crate::{ + algebra::*, + solver::{core::ScalingStrategy, CoreSettings}, +}; + +// ------------------------------------- +// Generalized Power Cone +// ------------------------------------- + +pub struct GenPowerCone { + // power defining the cone + α: Vec, + // gradient of the dual barrier at z + grad: Vec, + // holds copy of z at scaling point + z: Vec, + // dimension of u, w and their sum + pub dim1: usize, + pub dim2: usize, + dim: usize, + // central path parameter + pub μ: T, + // vectors for rank 3 update representation of Hs + pub p: Vec, + pub q: Vec, + pub r: Vec, + // first part of the diagonal + d1: Vec, + // additional scalar terms for rank-2 rep + d2: T, + // additional constant for initialization in the Newton-Raphson method + ψ: T, +} + +impl GenPowerCone +where + T: FloatT, +{ + pub fn new(α: Vec, dim1: usize, dim2: usize) -> Self { + let dim = dim1 + dim2; + + assert!(α.iter().all(|r| *r > T::zero())); // check all powers are greater than 0 + + // YC: should polish this check + let mut powerr = T::zero(); + α.iter().for_each(|x| powerr += *x); + powerr -= T::one(); + assert!(powerr.abs() < T::epsilon()); //check the sum of powers is 1 + + let ψ = T::one()/(α.sumsq()); + + Self { + α, + grad: vec![T::zero(); dim], + z: vec![T::zero(); dim], + dim1, + dim2, + dim, + μ: T::one(), + p: vec![T::zero(); dim], + q: vec![T::zero(); dim1], + r: vec![T::zero(); dim2], + d1: vec![T::zero(); dim1], + d2: T::zero(), + ψ, + } + } +} + + +impl Cone for GenPowerCone +where + T: FloatT, +{ + fn degree(&self) -> usize { + self.dim1 + 1 + } + + fn numel(&self) -> usize { + self.dim + } + + fn is_symmetric(&self) -> bool { + false + } + + fn rectify_equilibration(&self, δ: &mut [T], e: &[T]) -> bool { + δ.copy_from(e).recip().scale(e.mean()); + true // scalar equilibration + } + + fn margins(&mut self, _z: &mut [T], _pd: PrimalOrDualCone) -> (T, T) { + // We should never end up shifting to this cone, since + // asymmetric problems should always use unit_initialization + unreachable!(); + } + fn scaled_unit_shift(&self, _z: &mut [T], _α: T, _pd: PrimalOrDualCone) { + // We should never end up shifting to this cone, since + // asymmetric problems should always use unit_initialization + unreachable!(); + } + + fn unit_initialization(&self, z: &mut [T], s: &mut [T]) { + let α = & self.α; + + s[..α.len()].iter_mut().enumerate().for_each(|(i, s)| *s = (T::one() + α[i]).sqrt()); + s[α.len()..].iter_mut().for_each(|x| *x = T::zero()); + + z.copy_from(&s); + } + + fn set_identity_scaling(&mut self) { + // We should never use identity scaling because + // we never want to allow symmetric initialization + unreachable!(); + } + + fn update_scaling( + &mut self, + s: &[T], + z: &[T], + μ: T, + scaling_strategy: ScalingStrategy, + ) -> bool { + // update both gradient and Hessian for function f*(z) at the point z + self.update_dual_grad_H(z); + self.μ = μ; + + // K.z .= z + self.z.copy_from(z); + + true + } + + fn Hs_is_diagonal(&self) -> bool { + true + } + + fn get_Hs(&self, Hsblock: &mut [T]) { + // we are returning here the diagonal D = [d1; d2] block + let d1 = & self.d1; + Hsblock[..self.dim1].iter_mut().enumerate().for_each(|(i,x)| *x = self.μ*d1[i]); + let c = self.μ*self.d2; + Hsblock[self.dim1..].iter_mut().for_each(|x| *x = c); + } + + fn mul_Hs(&mut self, y: &mut [T], x: &[T], _work: &mut [T]) { + let d1 = & self.d1; + let d2 = self.d2; + let dim1 = self.dim1; + + let coef_p = self.p.dot(x); + let coef_q = self.q.dot(&x[..dim1]); + let coef_r = self.r.dot(&x[dim1..]); + + let x1 = & x[..dim1]; + let x2 = & x[dim1..]; + + y.iter_mut().enumerate().for_each(|(i,x)| *x = coef_p*self.p[i]); + y[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x += d1[i]*x1[i] - coef_q*self.q[i]); + y[dim1..].iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x += d2*x2[i] - coef_r*self.r[i]); + y.iter_mut().for_each(|x| *x *= self.μ); + + } + + fn affine_ds(&self, ds: &mut [T], s: &[T]) { + ds.copy_from(s); + } + + fn combined_ds_shift(&mut self, shift: &mut [T], step_z: &mut [T], step_s: &mut [T], σμ: T) { + //YC: No 3rd order correction at present + + let grad = & self.grad; + shift.iter_mut().enumerate().for_each(|(i,x)| *x = σμ*grad[i]); + } + + fn Δs_from_Δz_offset(&mut self, out: &mut [T], ds: &[T], _work: &mut [T], _z: &[T]) { + out.copy_from(ds); + } + + fn step_length( + &mut self, + dz: &[T], + ds: &[T], + z: &[T], + s: &[T], + settings: &CoreSettings, + αmax: T, + ) -> (T, T) { + let step = settings.linesearch_backtrack_step; + let αmin = settings.min_terminate_step_length; + + // final backtracked position + + let _is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; + let _is_dual_feasible_fcn = |s: &[T]| -> bool { self.is_dual_feasible(s) }; + + //YC: Do we need to care about the memory allocation time for wq? + let mut wq = Vec::with_capacity(self.dim); + + let αz = self.step_length_n_cone(&mut wq, dz, z, αmax, αmin, step, _is_dual_feasible_fcn); + let αs = self.step_length_n_cone(&mut wq, ds, s, αmax, αmin, step, _is_prim_feasible_fcn); + + (αz, αs) + } + + fn compute_barrier(&self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { + let mut barrier = T::zero(); + + //YC: Do we need to care about the memory allocation time for wq? + let mut wq = Vec::with_capacity(self.dim); + + wq.iter_mut().enumerate().for_each(|(i,x)| *x = z[i] + α * dz[i]); + barrier += self.barrier_dual(&wq); + + wq.iter_mut().enumerate().for_each(|(i,x)| *x = s[i] + α * ds[i]); + barrier += self.barrier_primal(&wq); + + barrier + } +} + +//------------------------------------- +// Dual scaling +//------------------------------------- + +//YC: better to define an additional trait "NonsymmetricCone" for it +impl NonsymmetricCone for GenPowerCone +where + T: FloatT, +{ + // Returns true if s is primal feasible + fn is_primal_feasible(& self, s: &[T]) -> bool + where + T: FloatT, + { + let α = & self.α; + let two: T = (2f64).as_T(); + let dim1 = self.dim1; + + if s[..dim1].iter().all(|x| *x > T::zero()) { + let mut res = T::zero(); + for i in 0..dim1 { + res += two * α[i] * s[i].logsafe() + } + res = T::exp(res) - s[dim1..].sumsq(); + + if res > T::zero() { + return true; + } + } + false + } + + // Returns true if z is dual feasible + fn is_dual_feasible(& self, z: &[T]) -> bool + where + T: FloatT, + { + let α = & self.α; + let two: T = (2.).as_T(); + let dim1 = self.dim1; + + if z[..dim1].iter().all(|x| *x > T::zero()) { + let mut res = T::zero(); + for i in 0..dim1 { + res += two * α[i] * (z[i]/α[i]).logsafe() + } + res = T::exp(res) - z[dim1..].sumsq(); + + if res > T::zero() { + return true; + } + } + false + } + + // Compute the primal gradient of f(s) at s + fn minus_gradient_primal(&self, s: &[T]) -> (T,T) + where + T: FloatT, + { + let α = & self.α; + let dim1 = self.dim1; + let two: T = (2.).as_T(); + + // unscaled phi + let mut phi = T::one(); + for i in 0..dim1 { + phi *= s[i].powf(two * α[i]); + } + + // obtain g1 from the Newton-Raphson method + let norm_r = s[dim1..].norm(); + let mut g1 = T::zero(); + + if norm_r > T::epsilon() { + g1 = _newton_raphson_genpowcone(norm_r,&s[..dim1],phi,α,self.ψ); + // minus_g.iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x = g1*s[i]/norm_r); + // minus_g[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x = -(T::one()+α[i]+α[i]*g1*norm_r)/s[i]); + } + + (g1,norm_r) + } + + fn update_dual_grad_H(&mut self, z: &[T]) { + let α = & self.α; + let dim1 = self.dim1; + let two: T = (2.).as_T(); + + let mut phi = T::one(); + for i in 0..dim1 { + phi *= (z[0] / α[i]).powf(two * α[i]) + } + let norm2w = z[dim1..].sumsq(); + let ζ = phi - norm2w; + assert!(ζ > T::zero()); + + // compute the gradient at z + let grad = &mut self.grad; + let τ = &mut self.q; + τ.iter_mut().enumerate().for_each(|(i,τ)| *τ = two*α[i]/z[i]); + grad[..dim1].iter_mut().enumerate().for_each(|(i,grad)| *grad = -τ[i]*phi/ζ - (T::one()-α[i])/z[i]); + grad.iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x = two*z[i]/ζ); + + // compute Hessian information at z + let p0 = (phi*(phi+norm2w)/two).sqrt(); + let p1 = -two*phi/p0; + let q0 = (ζ*phi/two).sqrt(); + let r1 = two*(ζ/(phi+norm2w)).sqrt(); + + // compute the diagonal d1,d2 + let d1 = &mut self.d1; + d1.iter_mut().enumerate().for_each(|(i,d1)| *d1 = τ[i]*phi/(ζ*z[i]) + (T::one()-α[i])/(z[i]*z[i])); + self.d2 = two/ζ; + + // compute p, q, r where τ shares memory with q + let c1 = p0/ζ; + let p = &mut self.p; + p[..dim1].copy_from(&τ); + p[..dim1].iter_mut().for_each(|x| *x *= c1); + let c2 = p1/ζ; + p[dim1..].copy_from(&z[self.dim1..]); + p[dim1..].iter_mut().for_each(|x| *x *= c2); + + let c3 = q0/ζ; + let q = &mut self.q; + q.iter_mut().for_each(|x| *x *= c3); + let c4 = r1/ζ; + let r = &mut self.r; + r.copy_from(&z[dim1..]); + r.iter_mut().for_each(|x| *x *= c4); + + } + + fn barrier_dual(&self, z: &[T]) -> T + where + T: FloatT, + { + // Dual barrier: + let α = & self.α; + let dim1 = self.dim1; + let two: T = (2.).as_T(); + let mut res = T::zero(); + + for i in 0..dim1 { + res += two * α[i] * (z[i]/α[i]).logsafe(); + } + res = T::exp(res) - z[dim1..].sumsq(); + + let mut barrier:T = -res.logsafe(); + z[..dim1].iter().enumerate().for_each(|(i,x)| barrier -= (*x).logsafe()*(T::one()-α[i])); + + barrier + } + + fn barrier_primal(&self, s: &[T]) -> T + where + T: FloatT, + { + // Primal barrier: f(s) = ⟨s,g(s)⟩ - f*(-g(s)) + // NB: ⟨s,g(s)⟩ = -(dim1+1) = - ν + let α = & self.α; + + //YC: Do we need to care about the memory allocation time for minus_q? + let (g1,norm_r) = self.minus_gradient_primal(s); + let mut minus_g = Vec::with_capacity(self.dim); + + let dim1 = self.dim1; + if norm_r > T::epsilon() { + minus_g.iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x = g1*s[i]/norm_r); + minus_g[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x = -(T::one()+α[i]+α[i]*g1*norm_r)/s[i]); + } else { + minus_g.iter_mut().skip(dim1).for_each(|x| *x = T::zero()); + minus_g[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x = -(T::one()+α[i])/s[i]); + } + + let minus_one = (-1.).as_T(); + minus_g.iter_mut().for_each(|x| *x *= minus_one); // add the sign to it, i.e. return -g + + let out = - self.barrier_dual(& minus_g) - self.degree().as_T(); + + out + } + +} + +// ---------------------------------------------- +// internal operations for generalized power cones + +// Newton-Raphson method: +// solve a one-dimensional equation f(x) = 0 +// x(k+1) = x(k) - f(x(k))/f'(x(k)) +// When we initialize x0 such that 0 < x0 < x* and f(x0) > 0, +// the Newton-Raphson method converges quadratically + +fn _newton_raphson_genpowcone(norm_r: T, p: &[T], phi: T, α: &Vec, ψ : T) -> T +where + T: FloatT, +{ + let two: T = (2.).as_T(); + + // init point x0: f(x0) > 0 + let x0 = -norm_r.recip() + (ψ*norm_r + ((phi/norm_r/norm_r + ψ*ψ -T::one())*phi).sqrt())/(phi - norm_r*norm_r); + + // function for f(x) = 0 + let f0 = { + |x: T| -> T { + let mut fval = -(two*x/norm_r + x*x).logsafe(); + α.iter().enumerate().for_each(|(i,& αi)| fval += two*αi*((x*norm_r + (T::one() + αi)/αi).logsafe() - p[i].logsafe())); + + fval + } + }; + + // first derivative + let f1 = { + |x: T| -> T { + let mut fval = -(two*x + two/norm_r)/(x*x + two*x/norm_r); + α.iter().for_each(|& αi| fval += two*(αi)*norm_r/(norm_r*x + (T::one()+αi)/αi)); + + fval + } + }; + _newton_raphson_onesided(x0, f0, f1) +} + +// YC: it is the duplicate of the same one for power cones. Can we unify them into a common one? +fn _newton_raphson_onesided(x0: T, f0: impl Fn(T) -> T, f1: impl Fn(T) -> T) -> T +where + T: FloatT, +{ + // implements NR method from a starting point assumed to be to the + // left of the true value. Once a negative step is encountered + // this function will halt regardless of the calculated correction. + + let mut x = x0; + let mut iter = 0; + + while iter < 100 { + iter += 1; + let dfdx = f1(x); + let dx = -f0(x) / dfdx; + + if (dx < T::epsilon()) + || (T::abs(dx / x) < T::sqrt(T::epsilon())) + || (T::abs(dfdx) < T::epsilon()) + { + break; + } + x += dx; + } + + x +} diff --git a/src/solver/core/cones/mod.rs b/src/solver/core/cones/mod.rs index caa31c5e..f7833da3 100644 --- a/src/solver/core/cones/mod.rs +++ b/src/solver/core/cones/mod.rs @@ -14,6 +14,7 @@ mod nonnegativecone; mod powcone; mod socone; mod zerocone; +mod genpowcone; // partially specialized traits and blanket implementataions mod exppow_common; mod symmetric_common; @@ -22,7 +23,7 @@ mod symmetric_common; use exppow_common::*; pub use { compositecone::*, expcone::*, nonnegativecone::*, powcone::*, socone::*, supportedcone::*, - symmetric_common::*, zerocone::*, + symmetric_common::*, zerocone::*, genpowcone::*, }; // only use PSD cones with SDP/Blas enabled diff --git a/src/solver/core/cones/supportedcone.rs b/src/solver/core/cones/supportedcone.rs index 33c03d23..aaf862e6 100644 --- a/src/solver/core/cones/supportedcone.rs +++ b/src/solver/core/cones/supportedcone.rs @@ -39,6 +39,10 @@ pub enum SupportedConeT { /// means that the variable is the upper triangle of an nxn matrix. #[cfg(feature = "sdp")] PSDTriangleConeT(usize), + /// The generalized power cone. + /// + /// The parameter indicates the power and dimensions. + GenPowerConeT(&[T],usize,usize), } impl SupportedConeT { @@ -55,6 +59,7 @@ impl SupportedConeT { SupportedConeT::PowerConeT(_) => 3, #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(dim) => triangular_number(*dim), + SupportedConeT::GenPowerConeT(_,dim1,dim2) => *dim1+*dim2, } } } @@ -81,6 +86,7 @@ pub fn make_cone(cone: SupportedConeT) -> SupportedCone { SupportedConeT::PowerConeT(α) => PowerCone::::new(α).into(), #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(dim) => PSDTriangleCone::::new(dim).into(), + SupportedConeT::GenPowerConeT(α, dim1, dim2) => GenPowerCone::::new(α, dim1, dim2).into(), } } @@ -103,6 +109,7 @@ where PowerCone(PowerCone), #[cfg(feature = "sdp")] PSDTriangleCone(PSDTriangleCone), + GenPowerCone(GenPowerCone), } // we put PSDTriangleCone in a Box above since it is by the @@ -131,6 +138,7 @@ pub(crate) enum SupportedConeTag { PowerCone, #[cfg(feature = "sdp")] PSDTriangleCone, + GenPowerCone, } pub(crate) trait SupportedConeAsTag { @@ -148,6 +156,7 @@ impl SupportedConeAsTag for SupportedConeT { SupportedConeT::PowerConeT(_) => SupportedConeTag::PowerCone, #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(_) => SupportedConeTag::PSDTriangleCone, + SupportedConeT::GenPowerConeT(_,_,_) => SupportedConeTag::GenPowerCone, } } } @@ -163,6 +172,7 @@ impl SupportedConeAsTag for SupportedCone { SupportedCone::PowerCone(_) => SupportedConeTag::PowerCone, #[cfg(feature = "sdp")] SupportedCone::PSDTriangleCone(_) => SupportedConeTag::PSDTriangleCone, + SupportedCone::GenPowerCone(_) => SupportedConeTag::GenPowerCone, } } } @@ -178,6 +188,7 @@ impl SupportedConeTag { SupportedConeTag::PowerCone => "PowerCone", #[cfg(feature = "sdp")] SupportedConeTag::PSDTriangleCone => "PSDTriangleCone", + SupportedConeTag::GenPowerCone => "GenPowerCone", } } } diff --git a/src/solver/core/kktsolvers/direct/quasidef/datamap.rs b/src/solver/core/kktsolvers/direct/quasidef/datamap.rs index d976d22d..8dfa42b7 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/datamap.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/datamap.rs @@ -12,6 +12,10 @@ pub struct LDLDataMap { pub SOC_u: Vec>, //off diag dense columns u pub SOC_v: Vec>, //off diag dense columns v pub SOC_D: Vec, //diag of just the sparse SOC expansion D + pub GenPow_p: Vec>, // off diag dense columns p + pub GenPow_q: Vec>, // off diag dense columns q + pub GenPow_r: Vec>, // off diag dense columns r + pub GenPow_D: Vec, // diag of just the sparse GenPow expansion D // all of above terms should be disjoint and their union // should cover all of the user data in the KKT matrix. Now @@ -40,14 +44,22 @@ impl LDLDataMap { // make an index for each of the Hs blocks for each cone let Hsblocks = allocate_kkt_Hsblocks::(cones); - // now do the SOC expansion pieces + // now do the SOC and generalized power expansion pieces let nsoc = cones.type_count(SupportedConeTag::SecondOrderCone); - let p = 2 * nsoc; - let SOC_D = vec![0; p]; + let psoc = 2 * nsoc; + let SOC_D = vec![0; psoc]; let mut SOC_u = Vec::>::with_capacity(nsoc); let mut SOC_v = Vec::>::with_capacity(nsoc); + let ngenpow = cones.type_count(SupportedConeTag::GenPowerCone); + let pgenpow = 3 * ngenpow; + let GenPow_D = vec![0; pgenpow]; + + let mut GenPow_p = Vec::>::with_capacity(ngenpow); + let mut GenPow_q = Vec::>::with_capacity(ngenpow); + let mut GenPow_r = Vec::>::with_capacity(ngenpow); + for cone in cones.iter() { // `cone` here will be of our SupportedCone enum wrapper, so // we see if we can extract a SecondOrderCone `soc` @@ -55,9 +67,15 @@ impl LDLDataMap { SOC_u.push(vec![0; soc.numel()]); SOC_v.push(vec![0; soc.numel()]); } + // Generalized power cones + if let SupportedCone::GenPowerCone(genpow) = cone { + GenPow_p.push(vec![0; genpow.numel()]); + GenPow_q.push(vec![0; genpow.dim1]); + GenPow_r.push(vec![0; genpow.dim2]); + } } - let diag_full = vec![0; m + n + p]; + let diag_full = vec![0; m + n + psoc + pgenpow]; Self { P, @@ -66,6 +84,10 @@ impl LDLDataMap { SOC_u, SOC_v, SOC_D, + GenPow_p, + GenPow_q, + GenPow_r, + GenPow_D, diagP, diag_full, } diff --git a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs index a26ac5ca..4908fce0 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs @@ -19,7 +19,8 @@ pub struct DirectLDLKKTSolver { // problem dimensions m: usize, n: usize, - p: usize, + p_soc: usize, + p_genpow: usize, // Left and right hand sides for solves x: Vec, @@ -64,17 +65,18 @@ where ) -> Self { // solving in sparse format. Need this many // extra variables for SOCs - let p = 2 * cones.type_count(SupportedConeTag::SecondOrderCone); + let p_soc = 2 * cones.type_count(SupportedConeTag::SecondOrderCone); + let p_genpow = 3 * cones.type_count(SupportedConeTag::GenPowerCone); // LHS/RHS/work for iterative refinement - let x = vec![T::zero(); n + m + p]; - let b = vec![T::zero(); n + m + p]; - let work1 = vec![T::zero(); n + m + p]; - let work2 = vec![T::zero(); n + m + p]; + let x = vec![T::zero(); n + m + p_soc + p_genpow]; + let b = vec![T::zero(); n + m + p_soc + p_genpow]; + let work1 = vec![T::zero(); n + m + p_soc + p_genpow]; + let work2 = vec![T::zero(); n + m + p_soc + p_genpow]; // the expected signs of D in LDL - let mut dsigns = vec![1_i8; n + m + p]; - _fill_signs(&mut dsigns, m, n, p); + let mut dsigns = vec![1_i8; n + m + p_soc + p_genpow]; + _fill_signs(&mut dsigns, m, n, p_soc, p_genpow); // updates to the diagonal of KKT will be // assigned here before updating matrix entries @@ -95,7 +97,8 @@ where Self { m, n, - p, + p_soc, + p_genpow, x, b, work1, @@ -125,7 +128,7 @@ where values.negate(); _update_values(&mut self.ldlsolver, &mut self.KKT, index, values); - // update the scaled u and v columns. + // SOC: update the scaled u and v columns. let mut cidx = 0; // which of the SOCs are we working on? for cone in cones.iter() { @@ -151,15 +154,45 @@ where } //end match } //end for + // GenPow: update the scaled p,q,r columns. + let mut cidx = 0; // which of the GenPows are we working on? + + for cone in cones.iter() { + // `cone` here will be of our SupportedCone enum wrapper, so + // we can extract a GenPowerCone `genpow` + if let SupportedCone::GenPowerCone(genpow) = cone { + let minus_sqrtμ = -genpow.μ.sqrt(); + + //off diagonal columns (or rows)s + let KKT = &mut self.KKT; + let ldlsolver = &mut self.ldlsolver; + + _update_values(ldlsolver, KKT, &map.GenPow_q[cidx], &genpow.q); + _update_values(ldlsolver, KKT, &map.GenPow_r[cidx], &genpow.r); + _update_values(ldlsolver, KKT, &map.GenPow_p[cidx], &genpow.p); + _scale_values(ldlsolver, KKT, &map.GenPow_q[cidx], minus_sqrtμ); + _scale_values(ldlsolver, KKT, &map.GenPow_r[cidx], minus_sqrtμ); + _scale_values(ldlsolver, KKT, &map.GenPow_p[cidx], minus_sqrtμ); + + //add η^2*(-1/1) to diagonal in the extended rows/cols + // YC: Is it a bug in SOC implementation? + _update_values(ldlsolver, KKT, &[map.GenPow_D[cidx * 3]], &[-T::one()]); + _update_values(ldlsolver, KKT, &[map.GenPow_D[cidx * 3 + 1]], &[-T::one()]); + _update_values(ldlsolver, KKT, &[map.GenPow_D[cidx * 3 + 2]], &[T::one()]); + + cidx += 1; + } //end match + } //end for + self.regularize_and_refactor(settings) } //end fn fn setrhs(&mut self, rhsx: &[T], rhsz: &[T]) { - let (m, n, p) = (self.m, self.n, self.p); + let (m, n, p, p_genpow) = (self.m, self.n, self.p, self.p_genpow); self.b[0..n].copy_from(rhsx); self.b[n..(n + m)].copy_from(rhsz); - self.b[n + m..(n + m + p)].fill(T::zero()); + self.b[n + m..(n + m + p + p_genpow)].fill(T::zero()); } fn solve( @@ -193,7 +226,7 @@ where // extra helper functions, not required for KKTSolver trait fn getlhs(&self, lhsx: Option<&mut [T]>, lhsz: Option<&mut [T]>) { let x = &self.x; - let (m, n, _p) = (self.m, self.n, self.p); + let (m, n) = (self.m, self.n); if let Some(v) = lhsx { v.copy_from(&x[0..n]); @@ -405,16 +438,28 @@ fn _scale_values_KKT(KKT: &mut CscMatrix, index: &[usize], scale: } } -fn _fill_signs(signs: &mut [i8], m: usize, n: usize, p: usize) { +fn _fill_signs(signs: &mut [i8], m: usize, n: usize, p_soc: usize, p_genpow: usize) { signs.fill(1); //flip expected negative signs of D in LDL signs[n..(n + m)].iter_mut().for_each(|x| *x = -*x); - //the trailing block of p entries should + //the trailing block of p_soc entries should //have alternating signs - signs[(n + m)..(n + m + p)] + signs[(n + m)..(n + m + p_soc)] .iter_mut() .step_by(2) .for_each(|x| *x = -*x); + + //the trailing block of p_genpow entries should + //have two - signs and one + sign alternatively + signs[(n + m + p_soc)..(n + m + p_soc + p_genpow)] + .iter_mut() + .step_by(3) + .for_each(|x| *x = -*x); + signs[(n + m + p_soc)..(n + m + p_soc + p_genpow)] + .iter_mut() + .skip(1) + .step_by(3) + .for_each(|x| *x = -*x); } diff --git a/src/solver/core/kktsolvers/direct/quasidef/utils.rs b/src/solver/core/kktsolvers/direct/quasidef/utils.rs index 3f07b55c..cb725d2c 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/utils.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/utils.rs @@ -26,8 +26,11 @@ pub fn assemble_kkt_matrix( shape: MatrixTriangle, ) -> (CscMatrix, LDLDataMap) { let (m, n) = (A.nrows(), P.nrows()); + let n_socs = cones.type_count(SupportedConeTag::SecondOrderCone); - let p = 2 * n_socs; + let p_socs = 2 * n_socs; + let n_genpows = cones.type_count(SupportedConeTag::GenPowerCone); + let p_genpows = 3 * n_genpows; let mut maps = LDLDataMap::new(P, A, cones); @@ -42,8 +45,16 @@ pub fn assemble_kkt_matrix( // counting elements in both columns let nnz_SOC_vecs = 2 * maps.SOC_u.iter().fold(0, |acc, block| acc + block.len()); + // entries in the dense columns p.q.r of the + // sparse generalized power expansion terms. + let nnz_GenPow_vecs = maps.GenPow_p.iter().fold(0, |acc, block| acc + block.len()) + + maps.GenPow_q.iter().fold(0, |acc, block| acc + block.len()) + + maps.GenPow_r.iter().fold(0, |acc, block| acc + block.len()); + //entries in the sparse SOC diagonal extension block let nnz_SOC_ext = maps.SOC_D.len(); + //entries in the sparse generalized power diagonal extension block + let nnz_GenPow_ext = maps.GenPow_D.len(); let nnzKKT = P.nnz() + // Number of elements in P n - // Number of elements in diagonal top left block @@ -51,13 +62,15 @@ pub fn assemble_kkt_matrix( A.nnz() + // Number of nonzeros in A nnz_Hsblocks + // Number of elements in diagonal below A' nnz_SOC_vecs + // Number of elements in sparse SOC off diagonal columns - nnz_SOC_ext; // Number of elements in diagonal of SOC extension + nnz_SOC_ext + // Number of elements in diagonal of SOC extension + nnz_GenPow_vecs + // Number of elements in sparse generalized power off diagonal columns + nnz_GenPow_ext; // Number of elements in diagonal of generalized power extension - let Kdim = m + n + p; + let Kdim = m + n + p_socs + p_genpows; let mut K = CscMatrix::::spalloc((Kdim, Kdim), nnzKKT); - _kkt_assemble_colcounts(&mut K, P, A, cones, (m, n, p), shape); - _kkt_assemble_fill(&mut K, &mut maps, P, A, cones, (m, n, p), shape); + _kkt_assemble_colcounts(&mut K, P, A, cones, (m, n, p_socs, p_genpows), shape); + _kkt_assemble_fill(&mut K, &mut maps, P, A, cones, (m, n, p_socs, p_genpows), shape); (K, maps) } @@ -67,10 +80,10 @@ fn _kkt_assemble_colcounts( P: &CscMatrix, A: &CscMatrix, cones: &CompositeCone, - mnp: (usize, usize, usize), + mnp: (usize, usize, usize, usize), shape: MatrixTriangle, ) { - let (m, n, p) = (mnp.0, mnp.1, mnp.2); + let (m, n, p_socs, p_genpows) = (mnp.0, mnp.1, mnp.2, mnp.3); // use K.p to hold nnz entries in each // column of the KKT matrix @@ -128,7 +141,41 @@ fn _kkt_assemble_colcounts( // add diagonal block in the lower RH corner // to allow for the diagonal terms in SOC expansion - K.colcount_diag(n + m, p); + K.colcount_diag(n + m, p_socs); + + // count dense columns for each generalized power cone + let mut genpowidx = 0; // which Genpow are we working on? + + for (i, cone) in cones.iter().enumerate() { + if let SupportedCone::GenPowerCone(GenPow) = cone { + // we will add the p,q,r columns for this cone + let nvars = GenPow.numel(); + let dim1 = GenPow.dim1; + let dim2 = GenPow.dim2; + let headidx = cones.rng_cones[i].start; + + // which column does q go into? + let col = m + n + 2 * socidx + 3 * genpowidx; + + match shape { + MatrixTriangle::Triu => { + K.colcount_colvec(dim1, headidx + n, col); // q column + K.colcount_colvec(dim2, headidx + n + dim1, col + 1); // r column + K.colcount_colvec(nvars, headidx + n, col + 2); // p column + } + MatrixTriangle::Tril => { + K.colcount_rowvec(dim1, col, headidx + n); // q row + K.colcount_rowvec(dim2, col + 1, headidx + n + dim1); // r row + K.colcount_rowvec(nvars, col + 2, headidx + n); // p row + } + } + genpowidx = genpowidx + 1; + } + } + + // add diagonal block in the lower RH corner + // to allow for the diagonal terms in generalized power expansion + K.colcount_diag(n + m + p_socs, p_genpows); } fn _kkt_assemble_fill( @@ -137,10 +184,10 @@ fn _kkt_assemble_fill( P: &CscMatrix, A: &CscMatrix, cones: &CompositeCone, - mnp: (usize, usize, usize), + mnp: (usize, usize, usize, usize), shape: MatrixTriangle, ) { - let (m, n, p) = (mnp.0, mnp.1, mnp.2); + let (m, n, p_socs, p_genpows) = (mnp.0, mnp.1, mnp.2, mnp.3); // cumsum total entries to convert to K.p K.colcount_to_colptr(); @@ -202,7 +249,41 @@ fn _kkt_assemble_fill( } // fill in SOC diagonal extension with diagonal of structural zeros - K.fill_diag(&mut maps.SOC_D, n + m, p); + K.fill_diag(&mut maps.SOC_D, n + m, p_socs); + + // fill in dense columns for each generalized power cone + let mut genpowidx = 0; //which generalized power cone are we working on? + + for (i, cone) in cones.iter().enumerate() { + if let SupportedCone::GenPowerCone(_) = cone { + let headidx = cones.rng_cones[i].start; + let dim1 = cone.dim1; + + // which column does q go into (if triu)? + let col = m + n + 2 * socidx + 3 * genpowidx; + + // fill structural zeros for p,q,r columns for this cone + match shape { + MatrixTriangle::Triu => { + K.fill_colvec(&mut maps.GenPow_q[genpowidx], headidx + n, col); //q + K.fill_colvec(&mut maps.GenPow_r[genpowidx], headidx + n + dim1, col + 1); //r + K.fill_colvec(&mut maps.GenPow_p[genpowidx], headidx + n, col + 2); //p + //v + } + MatrixTriangle::Tril => { + K.fill_rowvec(&mut maps.GenPow_q[genpowidx], col, headidx + n); //q + K.fill_rowvec(&mut maps.GenPow_r[genpowidx], col + 1, headidx + n + dim1); //r + K.fill_rowvec(&mut maps.GenPow_p[genpowidx], col + 2, headidx + n); //p + //v + } + } + + genpowidx += 1; + } + } + + // fill in SOC diagonal extension with diagonal of structural zeros + K.fill_diag(&mut maps.GenPow_D, n + m + p_socs, p_genpows); // backshift the colptrs to recover K.p again K.backshift_colptrs(); From 07168337db271066287043d6cc39f74c0d02eee8 Mon Sep 17 00:00:00 2001 From: Yuwen Chen Date: Fri, 11 Aug 2023 12:20:07 +0100 Subject: [PATCH 02/52] genpow draft --- src/solver/core/cones/supportedcone.rs | 4 ++-- .../core/kktsolvers/direct/quasidef/directldlkktsolver.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/solver/core/cones/supportedcone.rs b/src/solver/core/cones/supportedcone.rs index aaf862e6..c82e89be 100644 --- a/src/solver/core/cones/supportedcone.rs +++ b/src/solver/core/cones/supportedcone.rs @@ -11,7 +11,7 @@ use crate::algebra::triangular_number; /// API type describing the type of a conic constraint. /// -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub enum SupportedConeT { /// The zero cone (used for equality constraints). /// @@ -42,7 +42,7 @@ pub enum SupportedConeT { /// The generalized power cone. /// /// The parameter indicates the power and dimensions. - GenPowerConeT(&[T],usize,usize), + GenPowerConeT(Vec,usize,usize), } impl SupportedConeT { diff --git a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs index 4908fce0..aaf1f999 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs @@ -188,7 +188,7 @@ where } //end fn fn setrhs(&mut self, rhsx: &[T], rhsz: &[T]) { - let (m, n, p, p_genpow) = (self.m, self.n, self.p, self.p_genpow); + let (m, n, p, p_genpow) = (self.m, self.n, self.p_soc, self.p_genpow); self.b[0..n].copy_from(rhsx); self.b[n..(n + m)].copy_from(rhsz); From 457e6032bb89d2eb8e75ba3589a01db31d2b9048 Mon Sep 17 00:00:00 2001 From: Paul Goulart Date: Wed, 30 Aug 2023 13:11:50 +0100 Subject: [PATCH 03/52] fix LP style initialization (#41) --- src/solver/implementations/default/kktsystem.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/solver/implementations/default/kktsystem.rs b/src/solver/implementations/default/kktsystem.rs index 47fc1118..1c5b4032 100644 --- a/src/solver/implementations/default/kktsystem.rs +++ b/src/solver/implementations/default/kktsystem.rs @@ -209,7 +209,7 @@ where if data.P.nnz() == 0 { // LP initialization - // solve with [0;b] as a RHS to get (x,s) initializers + // solve with [0;b] as a RHS to get (x,-s) initializers // zero out any sparse cone variables at end self.workx.fill(T::zero()); self.workz.copy_from(&data.b); @@ -219,6 +219,7 @@ where Some(&mut variables.s), settings.core(), ); + variables.s.negate(); // solve with [-c;0] as a RHS to get z initializer // zero out any sparse cone variables at end From 5a932ce8fbca0b0181812e6f1a5b365f9e8ea910 Mon Sep 17 00:00:00 2001 From: Paul Goulart Date: Wed, 30 Aug 2023 13:13:12 +0100 Subject: [PATCH 04/52] faster convergence for equality constrained problems (#42) --- src/solver/implementations/default/info.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/solver/implementations/default/info.rs b/src/solver/implementations/default/info.rs index 0ab06fbf..1a5fcf31 100644 --- a/src/solver/implementations/default/info.rs +++ b/src/solver/implementations/default/info.rs @@ -157,7 +157,7 @@ where && (self.res_dual > self.prev_res_dual || self.res_primal > self.prev_res_primal) { // Poor progress at high tolerance. - if self.ktratio < T::epsilon() * (100.).as_T() + if self.ktratio < T::one() * (100.).as_T() && (self.prev_gap_abs < settings.tol_gap_abs || self.prev_gap_rel < settings.tol_gap_rel) { @@ -308,7 +308,7 @@ where pinf_status: SolverStatus, dinf_status: SolverStatus, ) { - if self.ktratio < tol_ktratio && self.is_solved(tol_gap_abs, tol_gap_rel, tol_feas) { + if self.ktratio <= T::one() && self.is_solved(tol_gap_abs, tol_gap_rel, tol_feas) { self.status = solved_status; //PJG hardcoded factor 1000 here should be fixed } else if self.ktratio > tol_ktratio.recip() * (1000.0).as_T() { From 278ea334a980ddb85b3096ecac3a94140cd36119 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Wed, 30 Aug 2023 05:14:34 -0700 Subject: [PATCH 05/52] Update LICENSE.md (#39) Updated license with group name --- LICENSE.md | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 261eeb9e..70703507 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -175,18 +175,7 @@ END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] + Copyright 2022 University of Oxford Control Group Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 50648547af21356a8e0693ab5d595a73d7227a3b Mon Sep 17 00:00:00 2001 From: Paul Goulart Date: Wed, 30 Aug 2023 13:31:16 +0100 Subject: [PATCH 06/52] Adds additional documentation, examples and user utilities (#43) From 23e36b4ea5f7eff930c8181b0798a50779d25c0a Mon Sep 17 00:00:00 2001 From: Paul Goulart Date: Wed, 30 Aug 2023 13:39:34 +0100 Subject: [PATCH 07/52] Enable print of internal timers through the Julia wrapper (#44) --- src/julia/ClarabelRs/src/interface.jl | 12 ++++++++++++ src/julia/interface.rs | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/julia/ClarabelRs/src/interface.jl b/src/julia/ClarabelRs/src/interface.jl index 23d53666..c17c20e2 100644 --- a/src/julia/ClarabelRs/src/interface.jl +++ b/src/julia/ClarabelRs/src/interface.jl @@ -29,6 +29,11 @@ function get_info(solver::Solver) end +function print_timers(solver::Solver) + + solver_print_timers_jlrs(solver::Solver) + +end # ------------------------------------- # Wrappers for rust-side interface @@ -80,6 +85,13 @@ function solver_get_info_jlrs(solver::Solver) end +function solver_print_timers_jlrs(solver::Solver) + + ccall(Libdl.dlsym(librust,:solver_print_timers_jlrs),Cvoid, + (Ptr{Cvoid},), solver.ptr) + +end + function solver_drop_jlrs(solver::Solver) ccall(Libdl.dlsym(librust,:solver_drop_jlrs),Cvoid, (Ptr{Cvoid},), solver.ptr) diff --git a/src/julia/interface.rs b/src/julia/interface.rs index 04a152cf..6ac88330 100644 --- a/src/julia/interface.rs +++ b/src/julia/interface.rs @@ -107,6 +107,20 @@ pub(crate) extern "C" fn solver_get_info_jlrs(ptr: *mut c_void) -> DefaultInfo timers.print(), + None => println!("No timer info available"), + } + + // don't drop, since the memory is owned by + // Julia and we might want to solve again + std::mem::forget(solver); +} + // safely drop a solver object through its pointer. // called by the Julia side finalizer when a solver // is out of scope From 4319ea5f98736177543ca1791ed9a8f9894d6ec0 Mon Sep 17 00:00:00 2001 From: Paul Goulart Date: Wed, 30 Aug 2023 13:56:48 +0100 Subject: [PATCH 08/52] update cargo keywords (#45) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c8f96cc8..5cd85612 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "Apache-2.0" description = "Clarabel Conic Interior Point Solver for Rust / Python" readme = "README.md" repository = "https://github.com/oxfordcontrol/Clarabel.rs" -keywords = ["convex", "optimization", "QP", "SOCP", "SDP"] +keywords = ["convex", "optimization", "conic", "solver", "linear-programming"] categories = ["mathematics"] #required for openssl to work on git actions From 6e832383b2caddcb9f075e515f35d762245e9712 Mon Sep 17 00:00:00 2001 From: Paul Goulart Date: Wed, 30 Aug 2023 19:58:42 +0100 Subject: [PATCH 09/52] Provides utility for making allocated cscmatrix from an adjoint. (#46) --- src/algebra/csc/core.rs | 69 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/src/algebra/csc/core.rs b/src/algebra/csc/core.rs index b863a71e..4dc17b20 100644 --- a/src/algebra/csc/core.rs +++ b/src/algebra/csc/core.rs @@ -11,7 +11,6 @@ use std::iter::zip; /// [2. 0. 6.] /// [0. 4. 7.] /// ``` -/// /// ```no_run /// use clarabel::algebra::CscMatrix; /// @@ -363,6 +362,53 @@ impl ShapedMatrix for CscMatrix { } } +/// Make a concrete [CscMatrix] from its [Adjoint]. This operation will +/// allocate a new matrix and copy the data from the adjoint. +/// +/// __Example usage__ : To construct the transpose of a 3 x 3 matrix: +/// ```text +/// A = [1., 0., 0.] +/// [2., 4., 0.] +/// [3., 5., 6.] +///``` +/// ```no_run +/// use clarabel::algebra::CscMatrix; +/// +/// let A : CscMatrix = (&[ +/// [1., 0., 0.], // +/// [2., 4., 0.], // +/// [3., 5., 6.], +/// ]).into(); +/// +/// let At = A.t(); //Adjoint form. Does not copy anything. +/// +/// let B : CscMatrix = At.into(); //Concrete form. Allocates and copies. +/// +/// assert_eq!(A, B); +/// +/// ``` +impl<'a, T> From>> for CscMatrix +where + T: FloatT, +{ + fn from(M: Adjoint<'a, CscMatrix>) -> CscMatrix { + let src = M.src; + + let (m, n) = (src.n, src.m); + let mut A = CscMatrix::spalloc((m, n), src.nnz()); + + //make dummy mapping indices since we don't care + //where the entries go + let mut amap = vec![0usize; src.nnz()]; + + A.colcount_block(src, 0, MatrixShape::T); + A.colcount_to_colptr(); + A.fill_block(src, &mut amap, 0, 0, MatrixShape::T); + A.backshift_colptrs(); + A + } +} + #[test] fn test_csc_from_slice_of_arrays() { let A = CscMatrix::new( @@ -418,3 +464,24 @@ fn test_csc_get_entry() { assert_eq!(A.get_entry((4, 3)), None); assert_eq!(A.get_entry((3, 4)), None); } + +#[test] +fn test_adjoint_into() { + let A: CscMatrix = (&[ + [1., 0., 0.], // + [2., 4., 0.], // + [3., 5., 6.], + ]) + .into(); + + let T: CscMatrix = (&[ + [1., 2., 3.], // + [0., 4., 5.], // + [0., 0., 6.], + ]) + .into(); + + let B: CscMatrix = A.t().into(); //Concrete form. Allocates and copies. + + assert_eq!(B, T); +} From 69bc6dc0f5b9489044f1c77b9c37d6fedce831d4 Mon Sep 17 00:00:00 2001 From: Paul Goulart Date: Thu, 31 Aug 2023 12:48:12 +0100 Subject: [PATCH 10/52] simplify relative error gap calc (#48) --- src/solver/implementations/default/info.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/solver/implementations/default/info.rs b/src/solver/implementations/default/info.rs index 1a5fcf31..e85cd9e3 100644 --- a/src/solver/implementations/default/info.rs +++ b/src/solver/implementations/default/info.rs @@ -122,16 +122,11 @@ where // absolute and relative gaps self.gap_abs = T::abs(self.cost_primal - self.cost_dual); - - if (self.cost_primal > T::zero()) && (self.cost_dual < T::zero()) { - self.gap_rel = T::max_value(); - } else { - self.gap_rel = self.gap_abs - / T::max( - T::one(), - T::min(T::abs(self.cost_primal), T::abs(self.cost_dual)), - ); - } + self.gap_rel = self.gap_abs + / T::max( + T::one(), + T::min(T::abs(self.cost_primal), T::abs(self.cost_dual)), + ); // κ/τ self.ktratio = variables.κ / variables.τ; From f6de6a9e2e37e128315aa9935136fe592222546c Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Fri, 1 Sep 2023 15:02:44 +0100 Subject: [PATCH 11/52] fix build failures. --- src/solver/core/cones/compositecone.rs | 2 +- src/solver/core/cones/genpowcone.rs | 207 +++++++++++------- src/solver/core/cones/supportedcone.rs | 35 +-- .../core/kktsolvers/direct/quasidef/utils.rs | 24 +- .../implementations/default/info_print.rs | 1 + 5 files changed, 166 insertions(+), 103 deletions(-) diff --git a/src/solver/core/cones/compositecone.rs b/src/solver/core/cones/compositecone.rs index 53a6e376..a85f7fb0 100644 --- a/src/solver/core/cones/compositecone.rs +++ b/src/solver/core/cones/compositecone.rs @@ -52,7 +52,7 @@ where // create cones with the given dims for t in types.iter() { //make a new cone - let cone = make_cone(*t); + let cone = make_cone(t); //update global problem symmetry _is_symmetric = _is_symmetric && cone.is_symmetric(); diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs index 22fd8349..45c79604 100644 --- a/src/solver/core/cones/genpowcone.rs +++ b/src/solver/core/cones/genpowcone.rs @@ -18,7 +18,7 @@ pub struct GenPowerCone { // dimension of u, w and their sum pub dim1: usize, pub dim2: usize, - dim: usize, + dim: usize, // central path parameter pub μ: T, // vectors for rank 3 update representation of Hs @@ -30,7 +30,7 @@ pub struct GenPowerCone { // additional scalar terms for rank-2 rep d2: T, // additional constant for initialization in the Newton-Raphson method - ψ: T, + ψ: T, } impl GenPowerCone @@ -40,15 +40,16 @@ where pub fn new(α: Vec, dim1: usize, dim2: usize) -> Self { let dim = dim1 + dim2; - assert!(α.iter().all(|r| *r > T::zero())); // check all powers are greater than 0 + //PJG : this check belongs elsewhere + assert!(α.iter().all(|r| *r > T::zero())); // check all powers are greater than 0 // YC: should polish this check let mut powerr = T::zero(); α.iter().for_each(|x| powerr += *x); powerr -= T::one(); - assert!(powerr.abs() < T::epsilon()); //check the sum of powers is 1 + assert!(powerr.abs() < T::epsilon()); //check the sum of powers is 1 - let ψ = T::one()/(α.sumsq()); + let ψ = T::one() / (α.sumsq()); Self { α, @@ -68,7 +69,6 @@ where } } - impl Cone for GenPowerCone where T: FloatT, @@ -102,9 +102,12 @@ where } fn unit_initialization(&self, z: &mut [T], s: &mut [T]) { - let α = & self.α; + let α = &self.α; - s[..α.len()].iter_mut().enumerate().for_each(|(i, s)| *s = (T::one() + α[i]).sqrt()); + s[..α.len()] + .iter_mut() + .enumerate() + .for_each(|(i, s)| *s = (T::one() + α[i]).sqrt()); s[α.len()..].iter_mut().for_each(|x| *x = T::zero()); z.copy_from(&s); @@ -118,10 +121,10 @@ where fn update_scaling( &mut self, - s: &[T], + _s: &[T], z: &[T], μ: T, - scaling_strategy: ScalingStrategy, + _scaling_strategy: ScalingStrategy, ) -> bool { // update both gradient and Hessian for function f*(z) at the point z self.update_dual_grad_H(z); @@ -138,41 +141,57 @@ where } fn get_Hs(&self, Hsblock: &mut [T]) { - // we are returning here the diagonal D = [d1; d2] block - let d1 = & self.d1; - Hsblock[..self.dim1].iter_mut().enumerate().for_each(|(i,x)| *x = self.μ*d1[i]); - let c = self.μ*self.d2; + // we are returning here the diagonal D = [d1; d2] block + let d1 = &self.d1; + Hsblock[..self.dim1] + .iter_mut() + .enumerate() + .for_each(|(i, x)| *x = self.μ * d1[i]); + let c = self.μ * self.d2; Hsblock[self.dim1..].iter_mut().for_each(|x| *x = c); } fn mul_Hs(&mut self, y: &mut [T], x: &[T], _work: &mut [T]) { - let d1 = & self.d1; + let d1 = &self.d1; let d2 = self.d2; let dim1 = self.dim1; let coef_p = self.p.dot(x); let coef_q = self.q.dot(&x[..dim1]); - let coef_r = self.r.dot(&x[dim1..]); - - let x1 = & x[..dim1]; - let x2 = & x[dim1..]; - - y.iter_mut().enumerate().for_each(|(i,x)| *x = coef_p*self.p[i]); - y[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x += d1[i]*x1[i] - coef_q*self.q[i]); - y[dim1..].iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x += d2*x2[i] - coef_r*self.r[i]); + let coef_r = self.r.dot(&x[dim1..]); + + let x1 = &x[..dim1]; + let x2 = &x[dim1..]; + + y.iter_mut() + .enumerate() + .for_each(|(i, x)| *x = coef_p * self.p[i]); + y[..dim1] + .iter_mut() + .enumerate() + .for_each(|(i, x)| *x += d1[i] * x1[i] - coef_q * self.q[i]); + y[dim1..] + .iter_mut() + .enumerate() + .skip(dim1) + .for_each(|(i, x)| *x += d2 * x2[i] - coef_r * self.r[i]); y.iter_mut().for_each(|x| *x *= self.μ); - } fn affine_ds(&self, ds: &mut [T], s: &[T]) { ds.copy_from(s); } - fn combined_ds_shift(&mut self, shift: &mut [T], step_z: &mut [T], step_s: &mut [T], σμ: T) { + fn combined_ds_shift( + &mut self, shift: &mut [T], _step_z: &mut [T], _step_s: &mut [T], σμ: T + ) { //YC: No 3rd order correction at present - let grad = & self.grad; - shift.iter_mut().enumerate().for_each(|(i,x)| *x = σμ*grad[i]); + let grad = &self.grad; + shift + .iter_mut() + .enumerate() + .for_each(|(i, x)| *x = σμ * grad[i]); } fn Δs_from_Δz_offset(&mut self, out: &mut [T], ds: &[T], _work: &mut [T], _z: &[T]) { @@ -211,10 +230,14 @@ where //YC: Do we need to care about the memory allocation time for wq? let mut wq = Vec::with_capacity(self.dim); - wq.iter_mut().enumerate().for_each(|(i,x)| *x = z[i] + α * dz[i]); + wq.iter_mut() + .enumerate() + .for_each(|(i, x)| *x = z[i] + α * dz[i]); barrier += self.barrier_dual(&wq); - wq.iter_mut().enumerate().for_each(|(i,x)| *x = s[i] + α * ds[i]); + wq.iter_mut() + .enumerate() + .for_each(|(i, x)| *x = s[i] + α * ds[i]); barrier += self.barrier_primal(&wq); barrier @@ -222,7 +245,7 @@ where } //------------------------------------- -// Dual scaling +// Dual scaling //------------------------------------- //YC: better to define an additional trait "NonsymmetricCone" for it @@ -231,11 +254,11 @@ where T: FloatT, { // Returns true if s is primal feasible - fn is_primal_feasible(& self, s: &[T]) -> bool + fn is_primal_feasible(&self, s: &[T]) -> bool where T: FloatT, { - let α = & self.α; + let α = &self.α; let two: T = (2f64).as_T(); let dim1 = self.dim1; @@ -254,18 +277,18 @@ where } // Returns true if z is dual feasible - fn is_dual_feasible(& self, z: &[T]) -> bool + fn is_dual_feasible(&self, z: &[T]) -> bool where T: FloatT, - { - let α = & self.α; + { + let α = &self.α; let two: T = (2.).as_T(); let dim1 = self.dim1; if z[..dim1].iter().all(|x| *x > T::zero()) { let mut res = T::zero(); for i in 0..dim1 { - res += two * α[i] * (z[i]/α[i]).logsafe() + res += two * α[i] * (z[i] / α[i]).logsafe() } res = T::exp(res) - z[dim1..].sumsq(); @@ -277,11 +300,11 @@ where } // Compute the primal gradient of f(s) at s - fn minus_gradient_primal(&self, s: &[T]) -> (T,T) + fn minus_gradient_primal(&self, s: &[T]) -> (T, T) where T: FloatT, { - let α = & self.α; + let α = &self.α; let dim1 = self.dim1; let two: T = (2.).as_T(); @@ -294,18 +317,18 @@ where // obtain g1 from the Newton-Raphson method let norm_r = s[dim1..].norm(); let mut g1 = T::zero(); - + if norm_r > T::epsilon() { - g1 = _newton_raphson_genpowcone(norm_r,&s[..dim1],phi,α,self.ψ); + g1 = _newton_raphson_genpowcone(norm_r, &s[..dim1], phi, α, self.ψ); // minus_g.iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x = g1*s[i]/norm_r); // minus_g[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x = -(T::one()+α[i]+α[i]*g1*norm_r)/s[i]); - } + } - (g1,norm_r) + (g1, norm_r) } fn update_dual_grad_H(&mut self, z: &[T]) { - let α = & self.α; + let α = &self.α; let dim1 = self.dim1; let two: T = (2.).as_T(); @@ -320,38 +343,47 @@ where // compute the gradient at z let grad = &mut self.grad; let τ = &mut self.q; - τ.iter_mut().enumerate().for_each(|(i,τ)| *τ = two*α[i]/z[i]); - grad[..dim1].iter_mut().enumerate().for_each(|(i,grad)| *grad = -τ[i]*phi/ζ - (T::one()-α[i])/z[i]); - grad.iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x = two*z[i]/ζ); - - // compute Hessian information at z - let p0 = (phi*(phi+norm2w)/two).sqrt(); - let p1 = -two*phi/p0; - let q0 = (ζ*phi/two).sqrt(); - let r1 = two*(ζ/(phi+norm2w)).sqrt(); + τ.iter_mut() + .enumerate() + .for_each(|(i, τ)| *τ = two * α[i] / z[i]); + grad[..dim1] + .iter_mut() + .enumerate() + .for_each(|(i, grad)| *grad = -τ[i] * phi / ζ - (T::one() - α[i]) / z[i]); + grad.iter_mut() + .enumerate() + .skip(dim1) + .for_each(|(i, x)| *x = two * z[i] / ζ); + + // compute Hessian information at z + let p0 = (phi * (phi + norm2w) / two).sqrt(); + let p1 = -two * phi / p0; + let q0 = (ζ * phi / two).sqrt(); + let r1 = two * (ζ / (phi + norm2w)).sqrt(); // compute the diagonal d1,d2 let d1 = &mut self.d1; - d1.iter_mut().enumerate().for_each(|(i,d1)| *d1 = τ[i]*phi/(ζ*z[i]) + (T::one()-α[i])/(z[i]*z[i])); - self.d2 = two/ζ; + d1.iter_mut() + .enumerate() + .for_each(|(i, d1)| *d1 = τ[i] * phi / (ζ * z[i]) + (T::one() - α[i]) / (z[i] * z[i])); + self.d2 = two / ζ; - // compute p, q, r where τ shares memory with q - let c1 = p0/ζ; + // compute p, q, r where τ shares memory with q + let c1 = p0 / ζ; let p = &mut self.p; p[..dim1].copy_from(&τ); p[..dim1].iter_mut().for_each(|x| *x *= c1); - let c2 = p1/ζ; + let c2 = p1 / ζ; p[dim1..].copy_from(&z[self.dim1..]); p[dim1..].iter_mut().for_each(|x| *x *= c2); - let c3 = q0/ζ; + let c3 = q0 / ζ; let q = &mut self.q; q.iter_mut().for_each(|x| *x *= c3); - let c4 = r1/ζ; + let c4 = r1 / ζ; let r = &mut self.r; r.copy_from(&z[dim1..]); r.iter_mut().for_each(|x| *x *= c4); - } fn barrier_dual(&self, z: &[T]) -> T @@ -359,18 +391,21 @@ where T: FloatT, { // Dual barrier: - let α = & self.α; + let α = &self.α; let dim1 = self.dim1; let two: T = (2.).as_T(); let mut res = T::zero(); for i in 0..dim1 { - res += two * α[i] * (z[i]/α[i]).logsafe(); + res += two * α[i] * (z[i] / α[i]).logsafe(); } res = T::exp(res) - z[dim1..].sumsq(); - let mut barrier:T = -res.logsafe(); - z[..dim1].iter().enumerate().for_each(|(i,x)| barrier -= (*x).logsafe()*(T::one()-α[i])); + let mut barrier: T = -res.logsafe(); + z[..dim1] + .iter() + .enumerate() + .for_each(|(i, x)| barrier -= (*x).logsafe() * (T::one() - α[i])); barrier } @@ -381,29 +416,38 @@ where { // Primal barrier: f(s) = ⟨s,g(s)⟩ - f*(-g(s)) // NB: ⟨s,g(s)⟩ = -(dim1+1) = - ν - let α = & self.α; + let α = &self.α; //YC: Do we need to care about the memory allocation time for minus_q? - let (g1,norm_r) = self.minus_gradient_primal(s); + let (g1, norm_r) = self.minus_gradient_primal(s); let mut minus_g = Vec::with_capacity(self.dim); let dim1 = self.dim1; if norm_r > T::epsilon() { - minus_g.iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x = g1*s[i]/norm_r); - minus_g[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x = -(T::one()+α[i]+α[i]*g1*norm_r)/s[i]); + minus_g + .iter_mut() + .enumerate() + .skip(dim1) + .for_each(|(i, x)| *x = g1 * s[i] / norm_r); + minus_g[..dim1] + .iter_mut() + .enumerate() + .for_each(|(i, x)| *x = -(T::one() + α[i] + α[i] * g1 * norm_r) / s[i]); } else { minus_g.iter_mut().skip(dim1).for_each(|x| *x = T::zero()); - minus_g[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x = -(T::one()+α[i])/s[i]); + minus_g[..dim1] + .iter_mut() + .enumerate() + .for_each(|(i, x)| *x = -(T::one() + α[i]) / s[i]); } let minus_one = (-1.).as_T(); - minus_g.iter_mut().for_each(|x| *x *= minus_one); // add the sign to it, i.e. return -g + minus_g.iter_mut().for_each(|x| *x *= minus_one); // add the sign to it, i.e. return -g - let out = - self.barrier_dual(& minus_g) - self.degree().as_T(); + let out = -self.barrier_dual(&minus_g) - self.degree().as_T(); out } - } // ---------------------------------------------- @@ -412,23 +456,27 @@ where // Newton-Raphson method: // solve a one-dimensional equation f(x) = 0 // x(k+1) = x(k) - f(x(k))/f'(x(k)) -// When we initialize x0 such that 0 < x0 < x* and f(x0) > 0, +// When we initialize x0 such that 0 < x0 < x* and f(x0) > 0, // the Newton-Raphson method converges quadratically -fn _newton_raphson_genpowcone(norm_r: T, p: &[T], phi: T, α: &Vec, ψ : T) -> T +fn _newton_raphson_genpowcone(norm_r: T, p: &[T], phi: T, α: &Vec, ψ: T) -> T where T: FloatT, { let two: T = (2.).as_T(); // init point x0: f(x0) > 0 - let x0 = -norm_r.recip() + (ψ*norm_r + ((phi/norm_r/norm_r + ψ*ψ -T::one())*phi).sqrt())/(phi - norm_r*norm_r); + let x0 = -norm_r.recip() + + (ψ * norm_r + ((phi / norm_r / norm_r + ψ * ψ - T::one()) * phi).sqrt()) + / (phi - norm_r * norm_r); // function for f(x) = 0 let f0 = { |x: T| -> T { - let mut fval = -(two*x/norm_r + x*x).logsafe(); - α.iter().enumerate().for_each(|(i,& αi)| fval += two*αi*((x*norm_r + (T::one() + αi)/αi).logsafe() - p[i].logsafe())); + let mut fval = -(two * x / norm_r + x * x).logsafe(); + α.iter().enumerate().for_each(|(i, &αi)| { + fval += two * αi * ((x * norm_r + (T::one() + αi) / αi).logsafe() - p[i].logsafe()) + }); fval } @@ -437,8 +485,9 @@ where // first derivative let f1 = { |x: T| -> T { - let mut fval = -(two*x + two/norm_r)/(x*x + two*x/norm_r); - α.iter().for_each(|& αi| fval += two*(αi)*norm_r/(norm_r*x + (T::one()+αi)/αi)); + let mut fval = -(two * x + two / norm_r) / (x * x + two * x / norm_r); + α.iter() + .for_each(|&αi| fval += two * (αi) * norm_r / (norm_r * x + (T::one() + αi) / αi)); fval } diff --git a/src/solver/core/cones/supportedcone.rs b/src/solver/core/cones/supportedcone.rs index c82e89be..c4168ea0 100644 --- a/src/solver/core/cones/supportedcone.rs +++ b/src/solver/core/cones/supportedcone.rs @@ -25,24 +25,25 @@ pub enum SupportedConeT { /// /// The parameter indicates the cones dimension. SecondOrderConeT(usize), - /// The exponential cone in R^3. - /// - /// This cone takes no parameters - ExponentialConeT(), /// The power cone in R^3. /// /// The parameter indicates the power. PowerConeT(T), + /// The generalized power cone. + /// + /// The parameter indicates the power and dimensions. + /// PJG: second dimensions is redundant to length(α) + GenPowerConeT(Vec, usize, usize), + /// The exponential cone in R^3. + /// + /// This cone takes no parameters + ExponentialConeT(), /// The positive semidefinite cone in triangular form. /// /// The parameter indicates the matrix dimension, i.e. size = n /// means that the variable is the upper triangle of an nxn matrix. #[cfg(feature = "sdp")] PSDTriangleConeT(usize), - /// The generalized power cone. - /// - /// The parameter indicates the power and dimensions. - GenPowerConeT(Vec,usize,usize), } impl SupportedConeT { @@ -59,7 +60,7 @@ impl SupportedConeT { SupportedConeT::PowerConeT(_) => 3, #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(dim) => triangular_number(*dim), - SupportedConeT::GenPowerConeT(_,dim1,dim2) => *dim1+*dim2, + SupportedConeT::GenPowerConeT(_, dim1, dim2) => *dim1 + *dim2, } } } @@ -77,16 +78,18 @@ where // for the constraint types, and then map them through // make_cone to get the internal cone representations. -pub fn make_cone(cone: SupportedConeT) -> SupportedCone { +pub fn make_cone(cone: &SupportedConeT) -> SupportedCone { match cone { - SupportedConeT::NonnegativeConeT(dim) => NonnegativeCone::::new(dim).into(), - SupportedConeT::ZeroConeT(dim) => ZeroCone::::new(dim).into(), - SupportedConeT::SecondOrderConeT(dim) => SecondOrderCone::::new(dim).into(), + SupportedConeT::NonnegativeConeT(dim) => NonnegativeCone::::new(*dim).into(), + SupportedConeT::ZeroConeT(dim) => ZeroCone::::new(*dim).into(), + SupportedConeT::SecondOrderConeT(dim) => SecondOrderCone::::new(*dim).into(), SupportedConeT::ExponentialConeT() => ExponentialCone::::new().into(), - SupportedConeT::PowerConeT(α) => PowerCone::::new(α).into(), + SupportedConeT::PowerConeT(α) => PowerCone::::new(*α).into(), #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(dim) => PSDTriangleCone::::new(dim).into(), - SupportedConeT::GenPowerConeT(α, dim1, dim2) => GenPowerCone::::new(α, dim1, dim2).into(), + SupportedConeT::GenPowerConeT(α, dim1, dim2) => { + GenPowerCone::::new((*α).clone(), *dim1, *dim2).into() + } } } @@ -156,7 +159,7 @@ impl SupportedConeAsTag for SupportedConeT { SupportedConeT::PowerConeT(_) => SupportedConeTag::PowerCone, #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(_) => SupportedConeTag::PSDTriangleCone, - SupportedConeT::GenPowerConeT(_,_,_) => SupportedConeTag::GenPowerCone, + SupportedConeT::GenPowerConeT(_, _, _) => SupportedConeTag::GenPowerCone, } } } diff --git a/src/solver/core/kktsolvers/direct/quasidef/utils.rs b/src/solver/core/kktsolvers/direct/quasidef/utils.rs index cb725d2c..3d2707a7 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/utils.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/utils.rs @@ -48,8 +48,8 @@ pub fn assemble_kkt_matrix( // entries in the dense columns p.q.r of the // sparse generalized power expansion terms. let nnz_GenPow_vecs = maps.GenPow_p.iter().fold(0, |acc, block| acc + block.len()) - + maps.GenPow_q.iter().fold(0, |acc, block| acc + block.len()) - + maps.GenPow_r.iter().fold(0, |acc, block| acc + block.len()); + + maps.GenPow_q.iter().fold(0, |acc, block| acc + block.len()) + + maps.GenPow_r.iter().fold(0, |acc, block| acc + block.len()); //entries in the sparse SOC diagonal extension block let nnz_SOC_ext = maps.SOC_D.len(); @@ -64,13 +64,21 @@ pub fn assemble_kkt_matrix( nnz_SOC_vecs + // Number of elements in sparse SOC off diagonal columns nnz_SOC_ext + // Number of elements in diagonal of SOC extension nnz_GenPow_vecs + // Number of elements in sparse generalized power off diagonal columns - nnz_GenPow_ext; // Number of elements in diagonal of generalized power extension + nnz_GenPow_ext; // Number of elements in diagonal of generalized power extension let Kdim = m + n + p_socs + p_genpows; let mut K = CscMatrix::::spalloc((Kdim, Kdim), nnzKKT); _kkt_assemble_colcounts(&mut K, P, A, cones, (m, n, p_socs, p_genpows), shape); - _kkt_assemble_fill(&mut K, &mut maps, P, A, cones, (m, n, p_socs, p_genpows), shape); + _kkt_assemble_fill( + &mut K, + &mut maps, + P, + A, + cones, + (m, n, p_socs, p_genpows), + shape, + ); (K, maps) } @@ -255,7 +263,7 @@ fn _kkt_assemble_fill( let mut genpowidx = 0; //which generalized power cone are we working on? for (i, cone) in cones.iter().enumerate() { - if let SupportedCone::GenPowerCone(_) = cone { + if let SupportedCone::GenPowerCone(cone) = cone { let headidx = cones.rng_cones[i].start; let dim1 = cone.dim1; @@ -267,13 +275,15 @@ fn _kkt_assemble_fill( MatrixTriangle::Triu => { K.fill_colvec(&mut maps.GenPow_q[genpowidx], headidx + n, col); //q K.fill_colvec(&mut maps.GenPow_r[genpowidx], headidx + n + dim1, col + 1); //r - K.fill_colvec(&mut maps.GenPow_p[genpowidx], headidx + n, col + 2); //p + K.fill_colvec(&mut maps.GenPow_p[genpowidx], headidx + n, col + 2); + //p //v } MatrixTriangle::Tril => { K.fill_rowvec(&mut maps.GenPow_q[genpowidx], col, headidx + n); //q K.fill_rowvec(&mut maps.GenPow_r[genpowidx], col + 1, headidx + n + dim1); //r - K.fill_rowvec(&mut maps.GenPow_p[genpowidx], col + 2, headidx + n); //p + K.fill_rowvec(&mut maps.GenPow_p[genpowidx], col + 2, headidx + n); + //p //v } } diff --git a/src/solver/implementations/default/info_print.rs b/src/solver/implementations/default/info_print.rs index f2089bc1..ce0fd4c4 100644 --- a/src/solver/implementations/default/info_print.rs +++ b/src/solver/implementations/default/info_print.rs @@ -58,6 +58,7 @@ where _print_conedims_by_type(cones, SupportedConeTag::SecondOrderCone); _print_conedims_by_type(cones, SupportedConeTag::ExponentialCone); _print_conedims_by_type(cones, SupportedConeTag::PowerCone); + _print_conedims_by_type(cones, SupportedConeTag::GenPowerCone); #[cfg(feature = "sdp")] _print_conedims_by_type(cones, SupportedConeTag::PSDTriangleCone); From 04c2fd897f2be19bd9f601995ef229e4ee0909bb Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Sat, 2 Sep 2023 16:47:32 +0100 Subject: [PATCH 12/52] fix Julia porting error --- src/solver/implementations/default/info.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solver/implementations/default/info.rs b/src/solver/implementations/default/info.rs index e85cd9e3..a6ab6a52 100644 --- a/src/solver/implementations/default/info.rs +++ b/src/solver/implementations/default/info.rs @@ -152,7 +152,7 @@ where && (self.res_dual > self.prev_res_dual || self.res_primal > self.prev_res_primal) { // Poor progress at high tolerance. - if self.ktratio < T::one() * (100.).as_T() + if self.ktratio < T::epsilon() * (100.).as_T() && (self.prev_gap_abs < settings.tol_gap_abs || self.prev_gap_rel < settings.tol_gap_rel) { From fb55eb41730b65116cec363e4195d0ddb7656262 Mon Sep 17 00:00:00 2001 From: Yuwen Chen Date: Mon, 7 Aug 2023 10:16:46 +0100 Subject: [PATCH 13/52] add generalized power cones, but haven't debugged it yet --- src/solver/core/cones/exppow_common.rs | 77 +++ src/solver/core/cones/genpowcone.rs | 476 ++++++++++++++++++ src/solver/core/cones/mod.rs | 3 +- src/solver/core/cones/supportedcone.rs | 11 + .../kktsolvers/direct/quasidef/datamap.rs | 30 +- .../direct/quasidef/directldlkktsolver.rs | 77 ++- .../core/kktsolvers/direct/quasidef/utils.rs | 103 +++- 7 files changed, 745 insertions(+), 32 deletions(-) create mode 100644 src/solver/core/cones/genpowcone.rs diff --git a/src/solver/core/cones/exppow_common.rs b/src/solver/core/cones/exppow_common.rs index 5ae6689b..e3908d6b 100644 --- a/src/solver/core/cones/exppow_common.rs +++ b/src/solver/core/cones/exppow_common.rs @@ -190,3 +190,80 @@ where α } } + + + +// -------------------------------------- +// Traits and blanket implementations for Generalized PowerCones +// ------------------------------------- + +// Operations supported on 3d nonsymmetrics only +pub(crate) trait NonsymmetricCone { + // Returns true if s is primal feasible + fn is_primal_feasible(& self, s: &[T]) -> bool; + + // Returns true if z is dual feasible + fn is_dual_feasible(& self, z: &[T]) -> bool; + + // Compute the primal gradient of f(s) at s + fn minus_gradient_primal(& self, s: &[T]) -> (T,T); + + fn update_dual_grad_H(&mut self, z: &[T]); + + fn barrier_dual(&self, z: &[T]) -> T; + + fn barrier_primal(&self, s: &[T]) -> T; +} + +#[allow(clippy::too_many_arguments)] +pub(crate) trait NonsymmetricConeUtils { + fn step_length_n_cone( + & self, + wq: &mut [T], + dq: &[T], + q: &[T], + α_init: T, + α_min: T, + backtrack: T, + is_in_cone_fcn: impl Fn(&[T]) -> bool, + ) -> T; +} + +impl NonsymmetricConeUtils for C +where + T: FloatT, + C: NonsymmetricCone, +{ + // find the maximum step length α≥0 so that + // q + α*dq stays in an exponential or power + // cone, or their respective dual cones. + fn step_length_n_cone( + & self, + wq: &mut [T], + dq: &[T], + q: &[T], + α_init: T, + α_min: T, + backtrack: T, + is_in_cone_fcn: impl Fn(&[T]) -> bool, + ) -> T { + let mut α = α_init; + + loop { + // wq = q + α*dq + for i in 0..q.len() { + wq[i] = q[i] + α * dq[i]; + } + + if is_in_cone_fcn(wq) { + break; + } + α *= backtrack; + if α < α_min { + α = T::zero(); + break; + } + } + α + } +} \ No newline at end of file diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs new file mode 100644 index 00000000..22fd8349 --- /dev/null +++ b/src/solver/core/cones/genpowcone.rs @@ -0,0 +1,476 @@ +use super::*; +use crate::{ + algebra::*, + solver::{core::ScalingStrategy, CoreSettings}, +}; + +// ------------------------------------- +// Generalized Power Cone +// ------------------------------------- + +pub struct GenPowerCone { + // power defining the cone + α: Vec, + // gradient of the dual barrier at z + grad: Vec, + // holds copy of z at scaling point + z: Vec, + // dimension of u, w and their sum + pub dim1: usize, + pub dim2: usize, + dim: usize, + // central path parameter + pub μ: T, + // vectors for rank 3 update representation of Hs + pub p: Vec, + pub q: Vec, + pub r: Vec, + // first part of the diagonal + d1: Vec, + // additional scalar terms for rank-2 rep + d2: T, + // additional constant for initialization in the Newton-Raphson method + ψ: T, +} + +impl GenPowerCone +where + T: FloatT, +{ + pub fn new(α: Vec, dim1: usize, dim2: usize) -> Self { + let dim = dim1 + dim2; + + assert!(α.iter().all(|r| *r > T::zero())); // check all powers are greater than 0 + + // YC: should polish this check + let mut powerr = T::zero(); + α.iter().for_each(|x| powerr += *x); + powerr -= T::one(); + assert!(powerr.abs() < T::epsilon()); //check the sum of powers is 1 + + let ψ = T::one()/(α.sumsq()); + + Self { + α, + grad: vec![T::zero(); dim], + z: vec![T::zero(); dim], + dim1, + dim2, + dim, + μ: T::one(), + p: vec![T::zero(); dim], + q: vec![T::zero(); dim1], + r: vec![T::zero(); dim2], + d1: vec![T::zero(); dim1], + d2: T::zero(), + ψ, + } + } +} + + +impl Cone for GenPowerCone +where + T: FloatT, +{ + fn degree(&self) -> usize { + self.dim1 + 1 + } + + fn numel(&self) -> usize { + self.dim + } + + fn is_symmetric(&self) -> bool { + false + } + + fn rectify_equilibration(&self, δ: &mut [T], e: &[T]) -> bool { + δ.copy_from(e).recip().scale(e.mean()); + true // scalar equilibration + } + + fn margins(&mut self, _z: &mut [T], _pd: PrimalOrDualCone) -> (T, T) { + // We should never end up shifting to this cone, since + // asymmetric problems should always use unit_initialization + unreachable!(); + } + fn scaled_unit_shift(&self, _z: &mut [T], _α: T, _pd: PrimalOrDualCone) { + // We should never end up shifting to this cone, since + // asymmetric problems should always use unit_initialization + unreachable!(); + } + + fn unit_initialization(&self, z: &mut [T], s: &mut [T]) { + let α = & self.α; + + s[..α.len()].iter_mut().enumerate().for_each(|(i, s)| *s = (T::one() + α[i]).sqrt()); + s[α.len()..].iter_mut().for_each(|x| *x = T::zero()); + + z.copy_from(&s); + } + + fn set_identity_scaling(&mut self) { + // We should never use identity scaling because + // we never want to allow symmetric initialization + unreachable!(); + } + + fn update_scaling( + &mut self, + s: &[T], + z: &[T], + μ: T, + scaling_strategy: ScalingStrategy, + ) -> bool { + // update both gradient and Hessian for function f*(z) at the point z + self.update_dual_grad_H(z); + self.μ = μ; + + // K.z .= z + self.z.copy_from(z); + + true + } + + fn Hs_is_diagonal(&self) -> bool { + true + } + + fn get_Hs(&self, Hsblock: &mut [T]) { + // we are returning here the diagonal D = [d1; d2] block + let d1 = & self.d1; + Hsblock[..self.dim1].iter_mut().enumerate().for_each(|(i,x)| *x = self.μ*d1[i]); + let c = self.μ*self.d2; + Hsblock[self.dim1..].iter_mut().for_each(|x| *x = c); + } + + fn mul_Hs(&mut self, y: &mut [T], x: &[T], _work: &mut [T]) { + let d1 = & self.d1; + let d2 = self.d2; + let dim1 = self.dim1; + + let coef_p = self.p.dot(x); + let coef_q = self.q.dot(&x[..dim1]); + let coef_r = self.r.dot(&x[dim1..]); + + let x1 = & x[..dim1]; + let x2 = & x[dim1..]; + + y.iter_mut().enumerate().for_each(|(i,x)| *x = coef_p*self.p[i]); + y[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x += d1[i]*x1[i] - coef_q*self.q[i]); + y[dim1..].iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x += d2*x2[i] - coef_r*self.r[i]); + y.iter_mut().for_each(|x| *x *= self.μ); + + } + + fn affine_ds(&self, ds: &mut [T], s: &[T]) { + ds.copy_from(s); + } + + fn combined_ds_shift(&mut self, shift: &mut [T], step_z: &mut [T], step_s: &mut [T], σμ: T) { + //YC: No 3rd order correction at present + + let grad = & self.grad; + shift.iter_mut().enumerate().for_each(|(i,x)| *x = σμ*grad[i]); + } + + fn Δs_from_Δz_offset(&mut self, out: &mut [T], ds: &[T], _work: &mut [T], _z: &[T]) { + out.copy_from(ds); + } + + fn step_length( + &mut self, + dz: &[T], + ds: &[T], + z: &[T], + s: &[T], + settings: &CoreSettings, + αmax: T, + ) -> (T, T) { + let step = settings.linesearch_backtrack_step; + let αmin = settings.min_terminate_step_length; + + // final backtracked position + + let _is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; + let _is_dual_feasible_fcn = |s: &[T]| -> bool { self.is_dual_feasible(s) }; + + //YC: Do we need to care about the memory allocation time for wq? + let mut wq = Vec::with_capacity(self.dim); + + let αz = self.step_length_n_cone(&mut wq, dz, z, αmax, αmin, step, _is_dual_feasible_fcn); + let αs = self.step_length_n_cone(&mut wq, ds, s, αmax, αmin, step, _is_prim_feasible_fcn); + + (αz, αs) + } + + fn compute_barrier(&self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { + let mut barrier = T::zero(); + + //YC: Do we need to care about the memory allocation time for wq? + let mut wq = Vec::with_capacity(self.dim); + + wq.iter_mut().enumerate().for_each(|(i,x)| *x = z[i] + α * dz[i]); + barrier += self.barrier_dual(&wq); + + wq.iter_mut().enumerate().for_each(|(i,x)| *x = s[i] + α * ds[i]); + barrier += self.barrier_primal(&wq); + + barrier + } +} + +//------------------------------------- +// Dual scaling +//------------------------------------- + +//YC: better to define an additional trait "NonsymmetricCone" for it +impl NonsymmetricCone for GenPowerCone +where + T: FloatT, +{ + // Returns true if s is primal feasible + fn is_primal_feasible(& self, s: &[T]) -> bool + where + T: FloatT, + { + let α = & self.α; + let two: T = (2f64).as_T(); + let dim1 = self.dim1; + + if s[..dim1].iter().all(|x| *x > T::zero()) { + let mut res = T::zero(); + for i in 0..dim1 { + res += two * α[i] * s[i].logsafe() + } + res = T::exp(res) - s[dim1..].sumsq(); + + if res > T::zero() { + return true; + } + } + false + } + + // Returns true if z is dual feasible + fn is_dual_feasible(& self, z: &[T]) -> bool + where + T: FloatT, + { + let α = & self.α; + let two: T = (2.).as_T(); + let dim1 = self.dim1; + + if z[..dim1].iter().all(|x| *x > T::zero()) { + let mut res = T::zero(); + for i in 0..dim1 { + res += two * α[i] * (z[i]/α[i]).logsafe() + } + res = T::exp(res) - z[dim1..].sumsq(); + + if res > T::zero() { + return true; + } + } + false + } + + // Compute the primal gradient of f(s) at s + fn minus_gradient_primal(&self, s: &[T]) -> (T,T) + where + T: FloatT, + { + let α = & self.α; + let dim1 = self.dim1; + let two: T = (2.).as_T(); + + // unscaled phi + let mut phi = T::one(); + for i in 0..dim1 { + phi *= s[i].powf(two * α[i]); + } + + // obtain g1 from the Newton-Raphson method + let norm_r = s[dim1..].norm(); + let mut g1 = T::zero(); + + if norm_r > T::epsilon() { + g1 = _newton_raphson_genpowcone(norm_r,&s[..dim1],phi,α,self.ψ); + // minus_g.iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x = g1*s[i]/norm_r); + // minus_g[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x = -(T::one()+α[i]+α[i]*g1*norm_r)/s[i]); + } + + (g1,norm_r) + } + + fn update_dual_grad_H(&mut self, z: &[T]) { + let α = & self.α; + let dim1 = self.dim1; + let two: T = (2.).as_T(); + + let mut phi = T::one(); + for i in 0..dim1 { + phi *= (z[0] / α[i]).powf(two * α[i]) + } + let norm2w = z[dim1..].sumsq(); + let ζ = phi - norm2w; + assert!(ζ > T::zero()); + + // compute the gradient at z + let grad = &mut self.grad; + let τ = &mut self.q; + τ.iter_mut().enumerate().for_each(|(i,τ)| *τ = two*α[i]/z[i]); + grad[..dim1].iter_mut().enumerate().for_each(|(i,grad)| *grad = -τ[i]*phi/ζ - (T::one()-α[i])/z[i]); + grad.iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x = two*z[i]/ζ); + + // compute Hessian information at z + let p0 = (phi*(phi+norm2w)/two).sqrt(); + let p1 = -two*phi/p0; + let q0 = (ζ*phi/two).sqrt(); + let r1 = two*(ζ/(phi+norm2w)).sqrt(); + + // compute the diagonal d1,d2 + let d1 = &mut self.d1; + d1.iter_mut().enumerate().for_each(|(i,d1)| *d1 = τ[i]*phi/(ζ*z[i]) + (T::one()-α[i])/(z[i]*z[i])); + self.d2 = two/ζ; + + // compute p, q, r where τ shares memory with q + let c1 = p0/ζ; + let p = &mut self.p; + p[..dim1].copy_from(&τ); + p[..dim1].iter_mut().for_each(|x| *x *= c1); + let c2 = p1/ζ; + p[dim1..].copy_from(&z[self.dim1..]); + p[dim1..].iter_mut().for_each(|x| *x *= c2); + + let c3 = q0/ζ; + let q = &mut self.q; + q.iter_mut().for_each(|x| *x *= c3); + let c4 = r1/ζ; + let r = &mut self.r; + r.copy_from(&z[dim1..]); + r.iter_mut().for_each(|x| *x *= c4); + + } + + fn barrier_dual(&self, z: &[T]) -> T + where + T: FloatT, + { + // Dual barrier: + let α = & self.α; + let dim1 = self.dim1; + let two: T = (2.).as_T(); + let mut res = T::zero(); + + for i in 0..dim1 { + res += two * α[i] * (z[i]/α[i]).logsafe(); + } + res = T::exp(res) - z[dim1..].sumsq(); + + let mut barrier:T = -res.logsafe(); + z[..dim1].iter().enumerate().for_each(|(i,x)| barrier -= (*x).logsafe()*(T::one()-α[i])); + + barrier + } + + fn barrier_primal(&self, s: &[T]) -> T + where + T: FloatT, + { + // Primal barrier: f(s) = ⟨s,g(s)⟩ - f*(-g(s)) + // NB: ⟨s,g(s)⟩ = -(dim1+1) = - ν + let α = & self.α; + + //YC: Do we need to care about the memory allocation time for minus_q? + let (g1,norm_r) = self.minus_gradient_primal(s); + let mut minus_g = Vec::with_capacity(self.dim); + + let dim1 = self.dim1; + if norm_r > T::epsilon() { + minus_g.iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x = g1*s[i]/norm_r); + minus_g[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x = -(T::one()+α[i]+α[i]*g1*norm_r)/s[i]); + } else { + minus_g.iter_mut().skip(dim1).for_each(|x| *x = T::zero()); + minus_g[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x = -(T::one()+α[i])/s[i]); + } + + let minus_one = (-1.).as_T(); + minus_g.iter_mut().for_each(|x| *x *= minus_one); // add the sign to it, i.e. return -g + + let out = - self.barrier_dual(& minus_g) - self.degree().as_T(); + + out + } + +} + +// ---------------------------------------------- +// internal operations for generalized power cones + +// Newton-Raphson method: +// solve a one-dimensional equation f(x) = 0 +// x(k+1) = x(k) - f(x(k))/f'(x(k)) +// When we initialize x0 such that 0 < x0 < x* and f(x0) > 0, +// the Newton-Raphson method converges quadratically + +fn _newton_raphson_genpowcone(norm_r: T, p: &[T], phi: T, α: &Vec, ψ : T) -> T +where + T: FloatT, +{ + let two: T = (2.).as_T(); + + // init point x0: f(x0) > 0 + let x0 = -norm_r.recip() + (ψ*norm_r + ((phi/norm_r/norm_r + ψ*ψ -T::one())*phi).sqrt())/(phi - norm_r*norm_r); + + // function for f(x) = 0 + let f0 = { + |x: T| -> T { + let mut fval = -(two*x/norm_r + x*x).logsafe(); + α.iter().enumerate().for_each(|(i,& αi)| fval += two*αi*((x*norm_r + (T::one() + αi)/αi).logsafe() - p[i].logsafe())); + + fval + } + }; + + // first derivative + let f1 = { + |x: T| -> T { + let mut fval = -(two*x + two/norm_r)/(x*x + two*x/norm_r); + α.iter().for_each(|& αi| fval += two*(αi)*norm_r/(norm_r*x + (T::one()+αi)/αi)); + + fval + } + }; + _newton_raphson_onesided(x0, f0, f1) +} + +// YC: it is the duplicate of the same one for power cones. Can we unify them into a common one? +fn _newton_raphson_onesided(x0: T, f0: impl Fn(T) -> T, f1: impl Fn(T) -> T) -> T +where + T: FloatT, +{ + // implements NR method from a starting point assumed to be to the + // left of the true value. Once a negative step is encountered + // this function will halt regardless of the calculated correction. + + let mut x = x0; + let mut iter = 0; + + while iter < 100 { + iter += 1; + let dfdx = f1(x); + let dx = -f0(x) / dfdx; + + if (dx < T::epsilon()) + || (T::abs(dx / x) < T::sqrt(T::epsilon())) + || (T::abs(dfdx) < T::epsilon()) + { + break; + } + x += dx; + } + + x +} diff --git a/src/solver/core/cones/mod.rs b/src/solver/core/cones/mod.rs index caa31c5e..f7833da3 100644 --- a/src/solver/core/cones/mod.rs +++ b/src/solver/core/cones/mod.rs @@ -14,6 +14,7 @@ mod nonnegativecone; mod powcone; mod socone; mod zerocone; +mod genpowcone; // partially specialized traits and blanket implementataions mod exppow_common; mod symmetric_common; @@ -22,7 +23,7 @@ mod symmetric_common; use exppow_common::*; pub use { compositecone::*, expcone::*, nonnegativecone::*, powcone::*, socone::*, supportedcone::*, - symmetric_common::*, zerocone::*, + symmetric_common::*, zerocone::*, genpowcone::*, }; // only use PSD cones with SDP/Blas enabled diff --git a/src/solver/core/cones/supportedcone.rs b/src/solver/core/cones/supportedcone.rs index 33c03d23..aaf862e6 100644 --- a/src/solver/core/cones/supportedcone.rs +++ b/src/solver/core/cones/supportedcone.rs @@ -39,6 +39,10 @@ pub enum SupportedConeT { /// means that the variable is the upper triangle of an nxn matrix. #[cfg(feature = "sdp")] PSDTriangleConeT(usize), + /// The generalized power cone. + /// + /// The parameter indicates the power and dimensions. + GenPowerConeT(&[T],usize,usize), } impl SupportedConeT { @@ -55,6 +59,7 @@ impl SupportedConeT { SupportedConeT::PowerConeT(_) => 3, #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(dim) => triangular_number(*dim), + SupportedConeT::GenPowerConeT(_,dim1,dim2) => *dim1+*dim2, } } } @@ -81,6 +86,7 @@ pub fn make_cone(cone: SupportedConeT) -> SupportedCone { SupportedConeT::PowerConeT(α) => PowerCone::::new(α).into(), #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(dim) => PSDTriangleCone::::new(dim).into(), + SupportedConeT::GenPowerConeT(α, dim1, dim2) => GenPowerCone::::new(α, dim1, dim2).into(), } } @@ -103,6 +109,7 @@ where PowerCone(PowerCone), #[cfg(feature = "sdp")] PSDTriangleCone(PSDTriangleCone), + GenPowerCone(GenPowerCone), } // we put PSDTriangleCone in a Box above since it is by the @@ -131,6 +138,7 @@ pub(crate) enum SupportedConeTag { PowerCone, #[cfg(feature = "sdp")] PSDTriangleCone, + GenPowerCone, } pub(crate) trait SupportedConeAsTag { @@ -148,6 +156,7 @@ impl SupportedConeAsTag for SupportedConeT { SupportedConeT::PowerConeT(_) => SupportedConeTag::PowerCone, #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(_) => SupportedConeTag::PSDTriangleCone, + SupportedConeT::GenPowerConeT(_,_,_) => SupportedConeTag::GenPowerCone, } } } @@ -163,6 +172,7 @@ impl SupportedConeAsTag for SupportedCone { SupportedCone::PowerCone(_) => SupportedConeTag::PowerCone, #[cfg(feature = "sdp")] SupportedCone::PSDTriangleCone(_) => SupportedConeTag::PSDTriangleCone, + SupportedCone::GenPowerCone(_) => SupportedConeTag::GenPowerCone, } } } @@ -178,6 +188,7 @@ impl SupportedConeTag { SupportedConeTag::PowerCone => "PowerCone", #[cfg(feature = "sdp")] SupportedConeTag::PSDTriangleCone => "PSDTriangleCone", + SupportedConeTag::GenPowerCone => "GenPowerCone", } } } diff --git a/src/solver/core/kktsolvers/direct/quasidef/datamap.rs b/src/solver/core/kktsolvers/direct/quasidef/datamap.rs index d976d22d..8dfa42b7 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/datamap.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/datamap.rs @@ -12,6 +12,10 @@ pub struct LDLDataMap { pub SOC_u: Vec>, //off diag dense columns u pub SOC_v: Vec>, //off diag dense columns v pub SOC_D: Vec, //diag of just the sparse SOC expansion D + pub GenPow_p: Vec>, // off diag dense columns p + pub GenPow_q: Vec>, // off diag dense columns q + pub GenPow_r: Vec>, // off diag dense columns r + pub GenPow_D: Vec, // diag of just the sparse GenPow expansion D // all of above terms should be disjoint and their union // should cover all of the user data in the KKT matrix. Now @@ -40,14 +44,22 @@ impl LDLDataMap { // make an index for each of the Hs blocks for each cone let Hsblocks = allocate_kkt_Hsblocks::(cones); - // now do the SOC expansion pieces + // now do the SOC and generalized power expansion pieces let nsoc = cones.type_count(SupportedConeTag::SecondOrderCone); - let p = 2 * nsoc; - let SOC_D = vec![0; p]; + let psoc = 2 * nsoc; + let SOC_D = vec![0; psoc]; let mut SOC_u = Vec::>::with_capacity(nsoc); let mut SOC_v = Vec::>::with_capacity(nsoc); + let ngenpow = cones.type_count(SupportedConeTag::GenPowerCone); + let pgenpow = 3 * ngenpow; + let GenPow_D = vec![0; pgenpow]; + + let mut GenPow_p = Vec::>::with_capacity(ngenpow); + let mut GenPow_q = Vec::>::with_capacity(ngenpow); + let mut GenPow_r = Vec::>::with_capacity(ngenpow); + for cone in cones.iter() { // `cone` here will be of our SupportedCone enum wrapper, so // we see if we can extract a SecondOrderCone `soc` @@ -55,9 +67,15 @@ impl LDLDataMap { SOC_u.push(vec![0; soc.numel()]); SOC_v.push(vec![0; soc.numel()]); } + // Generalized power cones + if let SupportedCone::GenPowerCone(genpow) = cone { + GenPow_p.push(vec![0; genpow.numel()]); + GenPow_q.push(vec![0; genpow.dim1]); + GenPow_r.push(vec![0; genpow.dim2]); + } } - let diag_full = vec![0; m + n + p]; + let diag_full = vec![0; m + n + psoc + pgenpow]; Self { P, @@ -66,6 +84,10 @@ impl LDLDataMap { SOC_u, SOC_v, SOC_D, + GenPow_p, + GenPow_q, + GenPow_r, + GenPow_D, diagP, diag_full, } diff --git a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs index a26ac5ca..4908fce0 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs @@ -19,7 +19,8 @@ pub struct DirectLDLKKTSolver { // problem dimensions m: usize, n: usize, - p: usize, + p_soc: usize, + p_genpow: usize, // Left and right hand sides for solves x: Vec, @@ -64,17 +65,18 @@ where ) -> Self { // solving in sparse format. Need this many // extra variables for SOCs - let p = 2 * cones.type_count(SupportedConeTag::SecondOrderCone); + let p_soc = 2 * cones.type_count(SupportedConeTag::SecondOrderCone); + let p_genpow = 3 * cones.type_count(SupportedConeTag::GenPowerCone); // LHS/RHS/work for iterative refinement - let x = vec![T::zero(); n + m + p]; - let b = vec![T::zero(); n + m + p]; - let work1 = vec![T::zero(); n + m + p]; - let work2 = vec![T::zero(); n + m + p]; + let x = vec![T::zero(); n + m + p_soc + p_genpow]; + let b = vec![T::zero(); n + m + p_soc + p_genpow]; + let work1 = vec![T::zero(); n + m + p_soc + p_genpow]; + let work2 = vec![T::zero(); n + m + p_soc + p_genpow]; // the expected signs of D in LDL - let mut dsigns = vec![1_i8; n + m + p]; - _fill_signs(&mut dsigns, m, n, p); + let mut dsigns = vec![1_i8; n + m + p_soc + p_genpow]; + _fill_signs(&mut dsigns, m, n, p_soc, p_genpow); // updates to the diagonal of KKT will be // assigned here before updating matrix entries @@ -95,7 +97,8 @@ where Self { m, n, - p, + p_soc, + p_genpow, x, b, work1, @@ -125,7 +128,7 @@ where values.negate(); _update_values(&mut self.ldlsolver, &mut self.KKT, index, values); - // update the scaled u and v columns. + // SOC: update the scaled u and v columns. let mut cidx = 0; // which of the SOCs are we working on? for cone in cones.iter() { @@ -151,15 +154,45 @@ where } //end match } //end for + // GenPow: update the scaled p,q,r columns. + let mut cidx = 0; // which of the GenPows are we working on? + + for cone in cones.iter() { + // `cone` here will be of our SupportedCone enum wrapper, so + // we can extract a GenPowerCone `genpow` + if let SupportedCone::GenPowerCone(genpow) = cone { + let minus_sqrtμ = -genpow.μ.sqrt(); + + //off diagonal columns (or rows)s + let KKT = &mut self.KKT; + let ldlsolver = &mut self.ldlsolver; + + _update_values(ldlsolver, KKT, &map.GenPow_q[cidx], &genpow.q); + _update_values(ldlsolver, KKT, &map.GenPow_r[cidx], &genpow.r); + _update_values(ldlsolver, KKT, &map.GenPow_p[cidx], &genpow.p); + _scale_values(ldlsolver, KKT, &map.GenPow_q[cidx], minus_sqrtμ); + _scale_values(ldlsolver, KKT, &map.GenPow_r[cidx], minus_sqrtμ); + _scale_values(ldlsolver, KKT, &map.GenPow_p[cidx], minus_sqrtμ); + + //add η^2*(-1/1) to diagonal in the extended rows/cols + // YC: Is it a bug in SOC implementation? + _update_values(ldlsolver, KKT, &[map.GenPow_D[cidx * 3]], &[-T::one()]); + _update_values(ldlsolver, KKT, &[map.GenPow_D[cidx * 3 + 1]], &[-T::one()]); + _update_values(ldlsolver, KKT, &[map.GenPow_D[cidx * 3 + 2]], &[T::one()]); + + cidx += 1; + } //end match + } //end for + self.regularize_and_refactor(settings) } //end fn fn setrhs(&mut self, rhsx: &[T], rhsz: &[T]) { - let (m, n, p) = (self.m, self.n, self.p); + let (m, n, p, p_genpow) = (self.m, self.n, self.p, self.p_genpow); self.b[0..n].copy_from(rhsx); self.b[n..(n + m)].copy_from(rhsz); - self.b[n + m..(n + m + p)].fill(T::zero()); + self.b[n + m..(n + m + p + p_genpow)].fill(T::zero()); } fn solve( @@ -193,7 +226,7 @@ where // extra helper functions, not required for KKTSolver trait fn getlhs(&self, lhsx: Option<&mut [T]>, lhsz: Option<&mut [T]>) { let x = &self.x; - let (m, n, _p) = (self.m, self.n, self.p); + let (m, n) = (self.m, self.n); if let Some(v) = lhsx { v.copy_from(&x[0..n]); @@ -405,16 +438,28 @@ fn _scale_values_KKT(KKT: &mut CscMatrix, index: &[usize], scale: } } -fn _fill_signs(signs: &mut [i8], m: usize, n: usize, p: usize) { +fn _fill_signs(signs: &mut [i8], m: usize, n: usize, p_soc: usize, p_genpow: usize) { signs.fill(1); //flip expected negative signs of D in LDL signs[n..(n + m)].iter_mut().for_each(|x| *x = -*x); - //the trailing block of p entries should + //the trailing block of p_soc entries should //have alternating signs - signs[(n + m)..(n + m + p)] + signs[(n + m)..(n + m + p_soc)] .iter_mut() .step_by(2) .for_each(|x| *x = -*x); + + //the trailing block of p_genpow entries should + //have two - signs and one + sign alternatively + signs[(n + m + p_soc)..(n + m + p_soc + p_genpow)] + .iter_mut() + .step_by(3) + .for_each(|x| *x = -*x); + signs[(n + m + p_soc)..(n + m + p_soc + p_genpow)] + .iter_mut() + .skip(1) + .step_by(3) + .for_each(|x| *x = -*x); } diff --git a/src/solver/core/kktsolvers/direct/quasidef/utils.rs b/src/solver/core/kktsolvers/direct/quasidef/utils.rs index 3f07b55c..cb725d2c 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/utils.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/utils.rs @@ -26,8 +26,11 @@ pub fn assemble_kkt_matrix( shape: MatrixTriangle, ) -> (CscMatrix, LDLDataMap) { let (m, n) = (A.nrows(), P.nrows()); + let n_socs = cones.type_count(SupportedConeTag::SecondOrderCone); - let p = 2 * n_socs; + let p_socs = 2 * n_socs; + let n_genpows = cones.type_count(SupportedConeTag::GenPowerCone); + let p_genpows = 3 * n_genpows; let mut maps = LDLDataMap::new(P, A, cones); @@ -42,8 +45,16 @@ pub fn assemble_kkt_matrix( // counting elements in both columns let nnz_SOC_vecs = 2 * maps.SOC_u.iter().fold(0, |acc, block| acc + block.len()); + // entries in the dense columns p.q.r of the + // sparse generalized power expansion terms. + let nnz_GenPow_vecs = maps.GenPow_p.iter().fold(0, |acc, block| acc + block.len()) + + maps.GenPow_q.iter().fold(0, |acc, block| acc + block.len()) + + maps.GenPow_r.iter().fold(0, |acc, block| acc + block.len()); + //entries in the sparse SOC diagonal extension block let nnz_SOC_ext = maps.SOC_D.len(); + //entries in the sparse generalized power diagonal extension block + let nnz_GenPow_ext = maps.GenPow_D.len(); let nnzKKT = P.nnz() + // Number of elements in P n - // Number of elements in diagonal top left block @@ -51,13 +62,15 @@ pub fn assemble_kkt_matrix( A.nnz() + // Number of nonzeros in A nnz_Hsblocks + // Number of elements in diagonal below A' nnz_SOC_vecs + // Number of elements in sparse SOC off diagonal columns - nnz_SOC_ext; // Number of elements in diagonal of SOC extension + nnz_SOC_ext + // Number of elements in diagonal of SOC extension + nnz_GenPow_vecs + // Number of elements in sparse generalized power off diagonal columns + nnz_GenPow_ext; // Number of elements in diagonal of generalized power extension - let Kdim = m + n + p; + let Kdim = m + n + p_socs + p_genpows; let mut K = CscMatrix::::spalloc((Kdim, Kdim), nnzKKT); - _kkt_assemble_colcounts(&mut K, P, A, cones, (m, n, p), shape); - _kkt_assemble_fill(&mut K, &mut maps, P, A, cones, (m, n, p), shape); + _kkt_assemble_colcounts(&mut K, P, A, cones, (m, n, p_socs, p_genpows), shape); + _kkt_assemble_fill(&mut K, &mut maps, P, A, cones, (m, n, p_socs, p_genpows), shape); (K, maps) } @@ -67,10 +80,10 @@ fn _kkt_assemble_colcounts( P: &CscMatrix, A: &CscMatrix, cones: &CompositeCone, - mnp: (usize, usize, usize), + mnp: (usize, usize, usize, usize), shape: MatrixTriangle, ) { - let (m, n, p) = (mnp.0, mnp.1, mnp.2); + let (m, n, p_socs, p_genpows) = (mnp.0, mnp.1, mnp.2, mnp.3); // use K.p to hold nnz entries in each // column of the KKT matrix @@ -128,7 +141,41 @@ fn _kkt_assemble_colcounts( // add diagonal block in the lower RH corner // to allow for the diagonal terms in SOC expansion - K.colcount_diag(n + m, p); + K.colcount_diag(n + m, p_socs); + + // count dense columns for each generalized power cone + let mut genpowidx = 0; // which Genpow are we working on? + + for (i, cone) in cones.iter().enumerate() { + if let SupportedCone::GenPowerCone(GenPow) = cone { + // we will add the p,q,r columns for this cone + let nvars = GenPow.numel(); + let dim1 = GenPow.dim1; + let dim2 = GenPow.dim2; + let headidx = cones.rng_cones[i].start; + + // which column does q go into? + let col = m + n + 2 * socidx + 3 * genpowidx; + + match shape { + MatrixTriangle::Triu => { + K.colcount_colvec(dim1, headidx + n, col); // q column + K.colcount_colvec(dim2, headidx + n + dim1, col + 1); // r column + K.colcount_colvec(nvars, headidx + n, col + 2); // p column + } + MatrixTriangle::Tril => { + K.colcount_rowvec(dim1, col, headidx + n); // q row + K.colcount_rowvec(dim2, col + 1, headidx + n + dim1); // r row + K.colcount_rowvec(nvars, col + 2, headidx + n); // p row + } + } + genpowidx = genpowidx + 1; + } + } + + // add diagonal block in the lower RH corner + // to allow for the diagonal terms in generalized power expansion + K.colcount_diag(n + m + p_socs, p_genpows); } fn _kkt_assemble_fill( @@ -137,10 +184,10 @@ fn _kkt_assemble_fill( P: &CscMatrix, A: &CscMatrix, cones: &CompositeCone, - mnp: (usize, usize, usize), + mnp: (usize, usize, usize, usize), shape: MatrixTriangle, ) { - let (m, n, p) = (mnp.0, mnp.1, mnp.2); + let (m, n, p_socs, p_genpows) = (mnp.0, mnp.1, mnp.2, mnp.3); // cumsum total entries to convert to K.p K.colcount_to_colptr(); @@ -202,7 +249,41 @@ fn _kkt_assemble_fill( } // fill in SOC diagonal extension with diagonal of structural zeros - K.fill_diag(&mut maps.SOC_D, n + m, p); + K.fill_diag(&mut maps.SOC_D, n + m, p_socs); + + // fill in dense columns for each generalized power cone + let mut genpowidx = 0; //which generalized power cone are we working on? + + for (i, cone) in cones.iter().enumerate() { + if let SupportedCone::GenPowerCone(_) = cone { + let headidx = cones.rng_cones[i].start; + let dim1 = cone.dim1; + + // which column does q go into (if triu)? + let col = m + n + 2 * socidx + 3 * genpowidx; + + // fill structural zeros for p,q,r columns for this cone + match shape { + MatrixTriangle::Triu => { + K.fill_colvec(&mut maps.GenPow_q[genpowidx], headidx + n, col); //q + K.fill_colvec(&mut maps.GenPow_r[genpowidx], headidx + n + dim1, col + 1); //r + K.fill_colvec(&mut maps.GenPow_p[genpowidx], headidx + n, col + 2); //p + //v + } + MatrixTriangle::Tril => { + K.fill_rowvec(&mut maps.GenPow_q[genpowidx], col, headidx + n); //q + K.fill_rowvec(&mut maps.GenPow_r[genpowidx], col + 1, headidx + n + dim1); //r + K.fill_rowvec(&mut maps.GenPow_p[genpowidx], col + 2, headidx + n); //p + //v + } + } + + genpowidx += 1; + } + } + + // fill in SOC diagonal extension with diagonal of structural zeros + K.fill_diag(&mut maps.GenPow_D, n + m + p_socs, p_genpows); // backshift the colptrs to recover K.p again K.backshift_colptrs(); From 315c4e01d452c8a3cda2e9e4f3b310f6a2f77ceb Mon Sep 17 00:00:00 2001 From: Yuwen Chen Date: Fri, 11 Aug 2023 12:20:07 +0100 Subject: [PATCH 14/52] genpow draft --- src/solver/core/cones/supportedcone.rs | 4 ++-- .../core/kktsolvers/direct/quasidef/directldlkktsolver.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/solver/core/cones/supportedcone.rs b/src/solver/core/cones/supportedcone.rs index aaf862e6..c82e89be 100644 --- a/src/solver/core/cones/supportedcone.rs +++ b/src/solver/core/cones/supportedcone.rs @@ -11,7 +11,7 @@ use crate::algebra::triangular_number; /// API type describing the type of a conic constraint. /// -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub enum SupportedConeT { /// The zero cone (used for equality constraints). /// @@ -42,7 +42,7 @@ pub enum SupportedConeT { /// The generalized power cone. /// /// The parameter indicates the power and dimensions. - GenPowerConeT(&[T],usize,usize), + GenPowerConeT(Vec,usize,usize), } impl SupportedConeT { diff --git a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs index 4908fce0..aaf1f999 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs @@ -188,7 +188,7 @@ where } //end fn fn setrhs(&mut self, rhsx: &[T], rhsz: &[T]) { - let (m, n, p, p_genpow) = (self.m, self.n, self.p, self.p_genpow); + let (m, n, p, p_genpow) = (self.m, self.n, self.p_soc, self.p_genpow); self.b[0..n].copy_from(rhsx); self.b[n..(n + m)].copy_from(rhsz); From df75069b561902750b12201c234a420a5af90152 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Fri, 1 Sep 2023 15:02:44 +0100 Subject: [PATCH 15/52] fix build failures. --- src/solver/core/cones/compositecone.rs | 2 +- src/solver/core/cones/genpowcone.rs | 207 +++++++++++------- src/solver/core/cones/supportedcone.rs | 35 +-- .../core/kktsolvers/direct/quasidef/utils.rs | 24 +- .../implementations/default/info_print.rs | 1 + 5 files changed, 166 insertions(+), 103 deletions(-) diff --git a/src/solver/core/cones/compositecone.rs b/src/solver/core/cones/compositecone.rs index 53a6e376..a85f7fb0 100644 --- a/src/solver/core/cones/compositecone.rs +++ b/src/solver/core/cones/compositecone.rs @@ -52,7 +52,7 @@ where // create cones with the given dims for t in types.iter() { //make a new cone - let cone = make_cone(*t); + let cone = make_cone(t); //update global problem symmetry _is_symmetric = _is_symmetric && cone.is_symmetric(); diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs index 22fd8349..45c79604 100644 --- a/src/solver/core/cones/genpowcone.rs +++ b/src/solver/core/cones/genpowcone.rs @@ -18,7 +18,7 @@ pub struct GenPowerCone { // dimension of u, w and their sum pub dim1: usize, pub dim2: usize, - dim: usize, + dim: usize, // central path parameter pub μ: T, // vectors for rank 3 update representation of Hs @@ -30,7 +30,7 @@ pub struct GenPowerCone { // additional scalar terms for rank-2 rep d2: T, // additional constant for initialization in the Newton-Raphson method - ψ: T, + ψ: T, } impl GenPowerCone @@ -40,15 +40,16 @@ where pub fn new(α: Vec, dim1: usize, dim2: usize) -> Self { let dim = dim1 + dim2; - assert!(α.iter().all(|r| *r > T::zero())); // check all powers are greater than 0 + //PJG : this check belongs elsewhere + assert!(α.iter().all(|r| *r > T::zero())); // check all powers are greater than 0 // YC: should polish this check let mut powerr = T::zero(); α.iter().for_each(|x| powerr += *x); powerr -= T::one(); - assert!(powerr.abs() < T::epsilon()); //check the sum of powers is 1 + assert!(powerr.abs() < T::epsilon()); //check the sum of powers is 1 - let ψ = T::one()/(α.sumsq()); + let ψ = T::one() / (α.sumsq()); Self { α, @@ -68,7 +69,6 @@ where } } - impl Cone for GenPowerCone where T: FloatT, @@ -102,9 +102,12 @@ where } fn unit_initialization(&self, z: &mut [T], s: &mut [T]) { - let α = & self.α; + let α = &self.α; - s[..α.len()].iter_mut().enumerate().for_each(|(i, s)| *s = (T::one() + α[i]).sqrt()); + s[..α.len()] + .iter_mut() + .enumerate() + .for_each(|(i, s)| *s = (T::one() + α[i]).sqrt()); s[α.len()..].iter_mut().for_each(|x| *x = T::zero()); z.copy_from(&s); @@ -118,10 +121,10 @@ where fn update_scaling( &mut self, - s: &[T], + _s: &[T], z: &[T], μ: T, - scaling_strategy: ScalingStrategy, + _scaling_strategy: ScalingStrategy, ) -> bool { // update both gradient and Hessian for function f*(z) at the point z self.update_dual_grad_H(z); @@ -138,41 +141,57 @@ where } fn get_Hs(&self, Hsblock: &mut [T]) { - // we are returning here the diagonal D = [d1; d2] block - let d1 = & self.d1; - Hsblock[..self.dim1].iter_mut().enumerate().for_each(|(i,x)| *x = self.μ*d1[i]); - let c = self.μ*self.d2; + // we are returning here the diagonal D = [d1; d2] block + let d1 = &self.d1; + Hsblock[..self.dim1] + .iter_mut() + .enumerate() + .for_each(|(i, x)| *x = self.μ * d1[i]); + let c = self.μ * self.d2; Hsblock[self.dim1..].iter_mut().for_each(|x| *x = c); } fn mul_Hs(&mut self, y: &mut [T], x: &[T], _work: &mut [T]) { - let d1 = & self.d1; + let d1 = &self.d1; let d2 = self.d2; let dim1 = self.dim1; let coef_p = self.p.dot(x); let coef_q = self.q.dot(&x[..dim1]); - let coef_r = self.r.dot(&x[dim1..]); - - let x1 = & x[..dim1]; - let x2 = & x[dim1..]; - - y.iter_mut().enumerate().for_each(|(i,x)| *x = coef_p*self.p[i]); - y[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x += d1[i]*x1[i] - coef_q*self.q[i]); - y[dim1..].iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x += d2*x2[i] - coef_r*self.r[i]); + let coef_r = self.r.dot(&x[dim1..]); + + let x1 = &x[..dim1]; + let x2 = &x[dim1..]; + + y.iter_mut() + .enumerate() + .for_each(|(i, x)| *x = coef_p * self.p[i]); + y[..dim1] + .iter_mut() + .enumerate() + .for_each(|(i, x)| *x += d1[i] * x1[i] - coef_q * self.q[i]); + y[dim1..] + .iter_mut() + .enumerate() + .skip(dim1) + .for_each(|(i, x)| *x += d2 * x2[i] - coef_r * self.r[i]); y.iter_mut().for_each(|x| *x *= self.μ); - } fn affine_ds(&self, ds: &mut [T], s: &[T]) { ds.copy_from(s); } - fn combined_ds_shift(&mut self, shift: &mut [T], step_z: &mut [T], step_s: &mut [T], σμ: T) { + fn combined_ds_shift( + &mut self, shift: &mut [T], _step_z: &mut [T], _step_s: &mut [T], σμ: T + ) { //YC: No 3rd order correction at present - let grad = & self.grad; - shift.iter_mut().enumerate().for_each(|(i,x)| *x = σμ*grad[i]); + let grad = &self.grad; + shift + .iter_mut() + .enumerate() + .for_each(|(i, x)| *x = σμ * grad[i]); } fn Δs_from_Δz_offset(&mut self, out: &mut [T], ds: &[T], _work: &mut [T], _z: &[T]) { @@ -211,10 +230,14 @@ where //YC: Do we need to care about the memory allocation time for wq? let mut wq = Vec::with_capacity(self.dim); - wq.iter_mut().enumerate().for_each(|(i,x)| *x = z[i] + α * dz[i]); + wq.iter_mut() + .enumerate() + .for_each(|(i, x)| *x = z[i] + α * dz[i]); barrier += self.barrier_dual(&wq); - wq.iter_mut().enumerate().for_each(|(i,x)| *x = s[i] + α * ds[i]); + wq.iter_mut() + .enumerate() + .for_each(|(i, x)| *x = s[i] + α * ds[i]); barrier += self.barrier_primal(&wq); barrier @@ -222,7 +245,7 @@ where } //------------------------------------- -// Dual scaling +// Dual scaling //------------------------------------- //YC: better to define an additional trait "NonsymmetricCone" for it @@ -231,11 +254,11 @@ where T: FloatT, { // Returns true if s is primal feasible - fn is_primal_feasible(& self, s: &[T]) -> bool + fn is_primal_feasible(&self, s: &[T]) -> bool where T: FloatT, { - let α = & self.α; + let α = &self.α; let two: T = (2f64).as_T(); let dim1 = self.dim1; @@ -254,18 +277,18 @@ where } // Returns true if z is dual feasible - fn is_dual_feasible(& self, z: &[T]) -> bool + fn is_dual_feasible(&self, z: &[T]) -> bool where T: FloatT, - { - let α = & self.α; + { + let α = &self.α; let two: T = (2.).as_T(); let dim1 = self.dim1; if z[..dim1].iter().all(|x| *x > T::zero()) { let mut res = T::zero(); for i in 0..dim1 { - res += two * α[i] * (z[i]/α[i]).logsafe() + res += two * α[i] * (z[i] / α[i]).logsafe() } res = T::exp(res) - z[dim1..].sumsq(); @@ -277,11 +300,11 @@ where } // Compute the primal gradient of f(s) at s - fn minus_gradient_primal(&self, s: &[T]) -> (T,T) + fn minus_gradient_primal(&self, s: &[T]) -> (T, T) where T: FloatT, { - let α = & self.α; + let α = &self.α; let dim1 = self.dim1; let two: T = (2.).as_T(); @@ -294,18 +317,18 @@ where // obtain g1 from the Newton-Raphson method let norm_r = s[dim1..].norm(); let mut g1 = T::zero(); - + if norm_r > T::epsilon() { - g1 = _newton_raphson_genpowcone(norm_r,&s[..dim1],phi,α,self.ψ); + g1 = _newton_raphson_genpowcone(norm_r, &s[..dim1], phi, α, self.ψ); // minus_g.iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x = g1*s[i]/norm_r); // minus_g[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x = -(T::one()+α[i]+α[i]*g1*norm_r)/s[i]); - } + } - (g1,norm_r) + (g1, norm_r) } fn update_dual_grad_H(&mut self, z: &[T]) { - let α = & self.α; + let α = &self.α; let dim1 = self.dim1; let two: T = (2.).as_T(); @@ -320,38 +343,47 @@ where // compute the gradient at z let grad = &mut self.grad; let τ = &mut self.q; - τ.iter_mut().enumerate().for_each(|(i,τ)| *τ = two*α[i]/z[i]); - grad[..dim1].iter_mut().enumerate().for_each(|(i,grad)| *grad = -τ[i]*phi/ζ - (T::one()-α[i])/z[i]); - grad.iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x = two*z[i]/ζ); - - // compute Hessian information at z - let p0 = (phi*(phi+norm2w)/two).sqrt(); - let p1 = -two*phi/p0; - let q0 = (ζ*phi/two).sqrt(); - let r1 = two*(ζ/(phi+norm2w)).sqrt(); + τ.iter_mut() + .enumerate() + .for_each(|(i, τ)| *τ = two * α[i] / z[i]); + grad[..dim1] + .iter_mut() + .enumerate() + .for_each(|(i, grad)| *grad = -τ[i] * phi / ζ - (T::one() - α[i]) / z[i]); + grad.iter_mut() + .enumerate() + .skip(dim1) + .for_each(|(i, x)| *x = two * z[i] / ζ); + + // compute Hessian information at z + let p0 = (phi * (phi + norm2w) / two).sqrt(); + let p1 = -two * phi / p0; + let q0 = (ζ * phi / two).sqrt(); + let r1 = two * (ζ / (phi + norm2w)).sqrt(); // compute the diagonal d1,d2 let d1 = &mut self.d1; - d1.iter_mut().enumerate().for_each(|(i,d1)| *d1 = τ[i]*phi/(ζ*z[i]) + (T::one()-α[i])/(z[i]*z[i])); - self.d2 = two/ζ; + d1.iter_mut() + .enumerate() + .for_each(|(i, d1)| *d1 = τ[i] * phi / (ζ * z[i]) + (T::one() - α[i]) / (z[i] * z[i])); + self.d2 = two / ζ; - // compute p, q, r where τ shares memory with q - let c1 = p0/ζ; + // compute p, q, r where τ shares memory with q + let c1 = p0 / ζ; let p = &mut self.p; p[..dim1].copy_from(&τ); p[..dim1].iter_mut().for_each(|x| *x *= c1); - let c2 = p1/ζ; + let c2 = p1 / ζ; p[dim1..].copy_from(&z[self.dim1..]); p[dim1..].iter_mut().for_each(|x| *x *= c2); - let c3 = q0/ζ; + let c3 = q0 / ζ; let q = &mut self.q; q.iter_mut().for_each(|x| *x *= c3); - let c4 = r1/ζ; + let c4 = r1 / ζ; let r = &mut self.r; r.copy_from(&z[dim1..]); r.iter_mut().for_each(|x| *x *= c4); - } fn barrier_dual(&self, z: &[T]) -> T @@ -359,18 +391,21 @@ where T: FloatT, { // Dual barrier: - let α = & self.α; + let α = &self.α; let dim1 = self.dim1; let two: T = (2.).as_T(); let mut res = T::zero(); for i in 0..dim1 { - res += two * α[i] * (z[i]/α[i]).logsafe(); + res += two * α[i] * (z[i] / α[i]).logsafe(); } res = T::exp(res) - z[dim1..].sumsq(); - let mut barrier:T = -res.logsafe(); - z[..dim1].iter().enumerate().for_each(|(i,x)| barrier -= (*x).logsafe()*(T::one()-α[i])); + let mut barrier: T = -res.logsafe(); + z[..dim1] + .iter() + .enumerate() + .for_each(|(i, x)| barrier -= (*x).logsafe() * (T::one() - α[i])); barrier } @@ -381,29 +416,38 @@ where { // Primal barrier: f(s) = ⟨s,g(s)⟩ - f*(-g(s)) // NB: ⟨s,g(s)⟩ = -(dim1+1) = - ν - let α = & self.α; + let α = &self.α; //YC: Do we need to care about the memory allocation time for minus_q? - let (g1,norm_r) = self.minus_gradient_primal(s); + let (g1, norm_r) = self.minus_gradient_primal(s); let mut minus_g = Vec::with_capacity(self.dim); let dim1 = self.dim1; if norm_r > T::epsilon() { - minus_g.iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x = g1*s[i]/norm_r); - minus_g[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x = -(T::one()+α[i]+α[i]*g1*norm_r)/s[i]); + minus_g + .iter_mut() + .enumerate() + .skip(dim1) + .for_each(|(i, x)| *x = g1 * s[i] / norm_r); + minus_g[..dim1] + .iter_mut() + .enumerate() + .for_each(|(i, x)| *x = -(T::one() + α[i] + α[i] * g1 * norm_r) / s[i]); } else { minus_g.iter_mut().skip(dim1).for_each(|x| *x = T::zero()); - minus_g[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x = -(T::one()+α[i])/s[i]); + minus_g[..dim1] + .iter_mut() + .enumerate() + .for_each(|(i, x)| *x = -(T::one() + α[i]) / s[i]); } let minus_one = (-1.).as_T(); - minus_g.iter_mut().for_each(|x| *x *= minus_one); // add the sign to it, i.e. return -g + minus_g.iter_mut().for_each(|x| *x *= minus_one); // add the sign to it, i.e. return -g - let out = - self.barrier_dual(& minus_g) - self.degree().as_T(); + let out = -self.barrier_dual(&minus_g) - self.degree().as_T(); out } - } // ---------------------------------------------- @@ -412,23 +456,27 @@ where // Newton-Raphson method: // solve a one-dimensional equation f(x) = 0 // x(k+1) = x(k) - f(x(k))/f'(x(k)) -// When we initialize x0 such that 0 < x0 < x* and f(x0) > 0, +// When we initialize x0 such that 0 < x0 < x* and f(x0) > 0, // the Newton-Raphson method converges quadratically -fn _newton_raphson_genpowcone(norm_r: T, p: &[T], phi: T, α: &Vec, ψ : T) -> T +fn _newton_raphson_genpowcone(norm_r: T, p: &[T], phi: T, α: &Vec, ψ: T) -> T where T: FloatT, { let two: T = (2.).as_T(); // init point x0: f(x0) > 0 - let x0 = -norm_r.recip() + (ψ*norm_r + ((phi/norm_r/norm_r + ψ*ψ -T::one())*phi).sqrt())/(phi - norm_r*norm_r); + let x0 = -norm_r.recip() + + (ψ * norm_r + ((phi / norm_r / norm_r + ψ * ψ - T::one()) * phi).sqrt()) + / (phi - norm_r * norm_r); // function for f(x) = 0 let f0 = { |x: T| -> T { - let mut fval = -(two*x/norm_r + x*x).logsafe(); - α.iter().enumerate().for_each(|(i,& αi)| fval += two*αi*((x*norm_r + (T::one() + αi)/αi).logsafe() - p[i].logsafe())); + let mut fval = -(two * x / norm_r + x * x).logsafe(); + α.iter().enumerate().for_each(|(i, &αi)| { + fval += two * αi * ((x * norm_r + (T::one() + αi) / αi).logsafe() - p[i].logsafe()) + }); fval } @@ -437,8 +485,9 @@ where // first derivative let f1 = { |x: T| -> T { - let mut fval = -(two*x + two/norm_r)/(x*x + two*x/norm_r); - α.iter().for_each(|& αi| fval += two*(αi)*norm_r/(norm_r*x + (T::one()+αi)/αi)); + let mut fval = -(two * x + two / norm_r) / (x * x + two * x / norm_r); + α.iter() + .for_each(|&αi| fval += two * (αi) * norm_r / (norm_r * x + (T::one() + αi) / αi)); fval } diff --git a/src/solver/core/cones/supportedcone.rs b/src/solver/core/cones/supportedcone.rs index c82e89be..c4168ea0 100644 --- a/src/solver/core/cones/supportedcone.rs +++ b/src/solver/core/cones/supportedcone.rs @@ -25,24 +25,25 @@ pub enum SupportedConeT { /// /// The parameter indicates the cones dimension. SecondOrderConeT(usize), - /// The exponential cone in R^3. - /// - /// This cone takes no parameters - ExponentialConeT(), /// The power cone in R^3. /// /// The parameter indicates the power. PowerConeT(T), + /// The generalized power cone. + /// + /// The parameter indicates the power and dimensions. + /// PJG: second dimensions is redundant to length(α) + GenPowerConeT(Vec, usize, usize), + /// The exponential cone in R^3. + /// + /// This cone takes no parameters + ExponentialConeT(), /// The positive semidefinite cone in triangular form. /// /// The parameter indicates the matrix dimension, i.e. size = n /// means that the variable is the upper triangle of an nxn matrix. #[cfg(feature = "sdp")] PSDTriangleConeT(usize), - /// The generalized power cone. - /// - /// The parameter indicates the power and dimensions. - GenPowerConeT(Vec,usize,usize), } impl SupportedConeT { @@ -59,7 +60,7 @@ impl SupportedConeT { SupportedConeT::PowerConeT(_) => 3, #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(dim) => triangular_number(*dim), - SupportedConeT::GenPowerConeT(_,dim1,dim2) => *dim1+*dim2, + SupportedConeT::GenPowerConeT(_, dim1, dim2) => *dim1 + *dim2, } } } @@ -77,16 +78,18 @@ where // for the constraint types, and then map them through // make_cone to get the internal cone representations. -pub fn make_cone(cone: SupportedConeT) -> SupportedCone { +pub fn make_cone(cone: &SupportedConeT) -> SupportedCone { match cone { - SupportedConeT::NonnegativeConeT(dim) => NonnegativeCone::::new(dim).into(), - SupportedConeT::ZeroConeT(dim) => ZeroCone::::new(dim).into(), - SupportedConeT::SecondOrderConeT(dim) => SecondOrderCone::::new(dim).into(), + SupportedConeT::NonnegativeConeT(dim) => NonnegativeCone::::new(*dim).into(), + SupportedConeT::ZeroConeT(dim) => ZeroCone::::new(*dim).into(), + SupportedConeT::SecondOrderConeT(dim) => SecondOrderCone::::new(*dim).into(), SupportedConeT::ExponentialConeT() => ExponentialCone::::new().into(), - SupportedConeT::PowerConeT(α) => PowerCone::::new(α).into(), + SupportedConeT::PowerConeT(α) => PowerCone::::new(*α).into(), #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(dim) => PSDTriangleCone::::new(dim).into(), - SupportedConeT::GenPowerConeT(α, dim1, dim2) => GenPowerCone::::new(α, dim1, dim2).into(), + SupportedConeT::GenPowerConeT(α, dim1, dim2) => { + GenPowerCone::::new((*α).clone(), *dim1, *dim2).into() + } } } @@ -156,7 +159,7 @@ impl SupportedConeAsTag for SupportedConeT { SupportedConeT::PowerConeT(_) => SupportedConeTag::PowerCone, #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(_) => SupportedConeTag::PSDTriangleCone, - SupportedConeT::GenPowerConeT(_,_,_) => SupportedConeTag::GenPowerCone, + SupportedConeT::GenPowerConeT(_, _, _) => SupportedConeTag::GenPowerCone, } } } diff --git a/src/solver/core/kktsolvers/direct/quasidef/utils.rs b/src/solver/core/kktsolvers/direct/quasidef/utils.rs index cb725d2c..3d2707a7 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/utils.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/utils.rs @@ -48,8 +48,8 @@ pub fn assemble_kkt_matrix( // entries in the dense columns p.q.r of the // sparse generalized power expansion terms. let nnz_GenPow_vecs = maps.GenPow_p.iter().fold(0, |acc, block| acc + block.len()) - + maps.GenPow_q.iter().fold(0, |acc, block| acc + block.len()) - + maps.GenPow_r.iter().fold(0, |acc, block| acc + block.len()); + + maps.GenPow_q.iter().fold(0, |acc, block| acc + block.len()) + + maps.GenPow_r.iter().fold(0, |acc, block| acc + block.len()); //entries in the sparse SOC diagonal extension block let nnz_SOC_ext = maps.SOC_D.len(); @@ -64,13 +64,21 @@ pub fn assemble_kkt_matrix( nnz_SOC_vecs + // Number of elements in sparse SOC off diagonal columns nnz_SOC_ext + // Number of elements in diagonal of SOC extension nnz_GenPow_vecs + // Number of elements in sparse generalized power off diagonal columns - nnz_GenPow_ext; // Number of elements in diagonal of generalized power extension + nnz_GenPow_ext; // Number of elements in diagonal of generalized power extension let Kdim = m + n + p_socs + p_genpows; let mut K = CscMatrix::::spalloc((Kdim, Kdim), nnzKKT); _kkt_assemble_colcounts(&mut K, P, A, cones, (m, n, p_socs, p_genpows), shape); - _kkt_assemble_fill(&mut K, &mut maps, P, A, cones, (m, n, p_socs, p_genpows), shape); + _kkt_assemble_fill( + &mut K, + &mut maps, + P, + A, + cones, + (m, n, p_socs, p_genpows), + shape, + ); (K, maps) } @@ -255,7 +263,7 @@ fn _kkt_assemble_fill( let mut genpowidx = 0; //which generalized power cone are we working on? for (i, cone) in cones.iter().enumerate() { - if let SupportedCone::GenPowerCone(_) = cone { + if let SupportedCone::GenPowerCone(cone) = cone { let headidx = cones.rng_cones[i].start; let dim1 = cone.dim1; @@ -267,13 +275,15 @@ fn _kkt_assemble_fill( MatrixTriangle::Triu => { K.fill_colvec(&mut maps.GenPow_q[genpowidx], headidx + n, col); //q K.fill_colvec(&mut maps.GenPow_r[genpowidx], headidx + n + dim1, col + 1); //r - K.fill_colvec(&mut maps.GenPow_p[genpowidx], headidx + n, col + 2); //p + K.fill_colvec(&mut maps.GenPow_p[genpowidx], headidx + n, col + 2); + //p //v } MatrixTriangle::Tril => { K.fill_rowvec(&mut maps.GenPow_q[genpowidx], col, headidx + n); //q K.fill_rowvec(&mut maps.GenPow_r[genpowidx], col + 1, headidx + n + dim1); //r - K.fill_rowvec(&mut maps.GenPow_p[genpowidx], col + 2, headidx + n); //p + K.fill_rowvec(&mut maps.GenPow_p[genpowidx], col + 2, headidx + n); + //p //v } } diff --git a/src/solver/implementations/default/info_print.rs b/src/solver/implementations/default/info_print.rs index f2089bc1..ce0fd4c4 100644 --- a/src/solver/implementations/default/info_print.rs +++ b/src/solver/implementations/default/info_print.rs @@ -58,6 +58,7 @@ where _print_conedims_by_type(cones, SupportedConeTag::SecondOrderCone); _print_conedims_by_type(cones, SupportedConeTag::ExponentialCone); _print_conedims_by_type(cones, SupportedConeTag::PowerCone); + _print_conedims_by_type(cones, SupportedConeTag::GenPowerCone); #[cfg(feature = "sdp")] _print_conedims_by_type(cones, SupportedConeTag::PSDTriangleCone); From d77704cb7847a6e8d8662a52b38d2451cd521975 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Mon, 4 Sep 2023 17:09:59 +0100 Subject: [PATCH 16/52] feature-genpowcone --- src/algebra/math_traits.rs | 5 +- src/algebra/vecmath.rs | 4 + src/solver/core/cones/expcone.rs | 4 +- src/solver/core/cones/exppow_common.rs | 126 ++++++---------------- src/solver/core/cones/genpowcone.rs | 142 +++++++++++++------------ 5 files changed, 121 insertions(+), 160 deletions(-) diff --git a/src/algebra/math_traits.rs b/src/algebra/math_traits.rs index 95e4a00e..68460c83 100644 --- a/src/algebra/math_traits.rs +++ b/src/algebra/math_traits.rs @@ -96,7 +96,10 @@ pub trait VectorMath { /// Standard Euclidian or 2-norm distance from `self` to `y` fn dist(&self, y: &Self) -> Self::T; - /// Sum of elements squared. + /// Sum of elements. + fn sum(&self) -> Self::T; + + /// Sum of squares of the elements. fn sumsq(&self) -> Self::T; /// 2-norm diff --git a/src/algebra/vecmath.rs b/src/algebra/vecmath.rs index e54a1b52..57d4cf01 100644 --- a/src/algebra/vecmath.rs +++ b/src/algebra/vecmath.rs @@ -101,6 +101,10 @@ impl VectorMath for [T] { T::sqrt(dist2) } + fn sum(&self) -> T { + self.iter().fold(T::zero(), |acc, &x| acc + x) + } + fn sumsq(&self) -> T { self.dot(self) } diff --git a/src/solver/core/cones/expcone.rs b/src/solver/core/cones/expcone.rs index c38b1506..958c6b29 100644 --- a/src/solver/core/cones/expcone.rs +++ b/src/solver/core/cones/expcone.rs @@ -153,8 +153,8 @@ where let _is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; let _is_dual_feasible_fcn = |s: &[T]| -> bool { self.is_dual_feasible(s) }; - let αz = self.step_length_3d_cone(wq, dz, z, αmax, αmin, step, _is_dual_feasible_fcn); - let αs = self.step_length_3d_cone(wq, ds, s, αmax, αmin, step, _is_prim_feasible_fcn); + let αz = self.step_length_cone(wq, dz, z, αmax, αmin, step, _is_dual_feasible_fcn); + let αs = self.step_length_cone(wq, ds, s, αmax, αmin, step, _is_prim_feasible_fcn); (αz, αs) } diff --git a/src/solver/core/cones/exppow_common.rs b/src/solver/core/cones/exppow_common.rs index e3908d6b..21b81e2e 100644 --- a/src/solver/core/cones/exppow_common.rs +++ b/src/solver/core/cones/exppow_common.rs @@ -1,20 +1,16 @@ use crate::{algebra::*, solver::core::ScalingStrategy}; // -------------------------------------- -// Traits and blanket implementations for Exponential and PowerCones +// Traits and blanket implementations for Exponential, 3D Power and ND Power Cones // ------------------------------------- - -// Operations supported on 3d nonsymmetrics only -pub(crate) trait Nonsymmetric3DCone { +// Operations supported on all nonsymmetric cones +pub(crate) trait NonsymmetricCone { // Returns true if s is primal feasible fn is_primal_feasible(&self, s: &[T]) -> bool; // Returns true if z is dual feasible fn is_dual_feasible(&self, z: &[T]) -> bool; - // Compute the primal gradient of f(s) at s - fn gradient_primal(&self, s: &[T]) -> [T; 3]; - fn update_dual_grad_H(&mut self, z: &[T]); fn barrier_dual(&self, z: &[T]) -> T; @@ -22,6 +18,21 @@ pub(crate) trait Nonsymmetric3DCone { fn barrier_primal(&self, s: &[T]) -> T; fn higher_correction(&mut self, η: &mut [T; 3], ds: &[T], v: &[T]); +} + +// -------------------------------------- +// Traits and blanket implementations for Exponential, 3D Power Cones +// ------------------------------------- +#[allow(clippy::too_many_arguments)] +pub(crate) trait Nonsymmetric3DCone { + fn update_Hs(&mut self, s: &[T], z: &[T], μ: T, scaling_strategy: ScalingStrategy); + + // Compute the primal gradient of f(s) at s + fn gradient_primal(&self, s: &[T]) -> [T; 3]; + + fn use_dual_scaling(&mut self, μ: T); + + fn use_primal_dual_scaling(&mut self, s: &[T], z: &[T]); // we can't mutably borrow individual fields through getter methods, // so we have this one method to borrow them simultaneously. @@ -36,39 +47,11 @@ pub(crate) trait Nonsymmetric3DCone { ); } -#[allow(clippy::too_many_arguments)] -pub(crate) trait Nonsymmetric3DConeUtils { - fn update_Hs(&mut self, s: &[T], z: &[T], μ: T, scaling_strategy: ScalingStrategy); - - fn use_dual_scaling(&mut self, μ: T); - - fn use_primal_dual_scaling(&mut self, s: &[T], z: &[T]); - - fn step_length_3d_cone( - &self, - wq: &mut [T], - dq: &[T], - q: &[T], - α_init: T, - α_min: T, - backtrack: T, - is_in_cone_fcn: impl Fn(&[T]) -> bool, - ) -> T; -} - -impl Nonsymmetric3DConeUtils for C +impl Nonsymmetric3DCone for C where T: FloatT, C: Nonsymmetric3DCone, { - // find the maximum step length α≥0 so that - // q + α*dq stays in an exponential or power - // cone, or their respective dual cones. - // - // NB: Not for use as a general checking - // function because cone lengths are hardcoded - // to R^3 for faster execution. - fn update_Hs(&mut self, s: &[T], z: &[T], μ: T, scaling_strategy: ScalingStrategy) { // Choose the scaling strategy if scaling_strategy == ScalingStrategy::Dual { @@ -159,66 +142,24 @@ where self.use_dual_scaling(μ); } } - - fn step_length_3d_cone( - &self, - wq: &mut [T], - dq: &[T], - q: &[T], - α_init: T, - α_min: T, - backtrack: T, - is_in_cone_fcn: impl Fn(&[T]) -> bool, - ) -> T { - let mut α = α_init; - - loop { - // wq = q + α*dq - for i in 0..3 { - wq[i] = q[i] + α * dq[i]; - } - - if is_in_cone_fcn(wq) { - break; - } - α *= backtrack; - if α < α_min { - α = T::zero(); - break; - } - } - α - } } - - // -------------------------------------- -// Traits and blanket implementations for Generalized PowerCones +// Traits Generalized PowerCones // ------------------------------------- -// Operations supported on 3d nonsymmetrics only -pub(crate) trait NonsymmetricCone { - // Returns true if s is primal feasible - fn is_primal_feasible(& self, s: &[T]) -> bool; - - // Returns true if z is dual feasible - fn is_dual_feasible(& self, z: &[T]) -> bool; - +// Operations supported on ND nonsymmetrics only. Note this +// differs from the 3D cone in particular because we don't +// return a 3D tuple for the primal gradient. +pub(crate) trait NonsymmetricNDCone { // Compute the primal gradient of f(s) at s - fn minus_gradient_primal(& self, s: &[T]) -> (T,T); - - fn update_dual_grad_H(&mut self, z: &[T]); - - fn barrier_dual(&self, z: &[T]) -> T; - - fn barrier_primal(&self, s: &[T]) -> T; + fn minus_gradient_primal(&self, s: &[T]) -> (T, T); } #[allow(clippy::too_many_arguments)] pub(crate) trait NonsymmetricConeUtils { - fn step_length_n_cone( - & self, + fn step_length_cone( + &self, wq: &mut [T], dq: &[T], q: &[T], @@ -229,16 +170,19 @@ pub(crate) trait NonsymmetricConeUtils { ) -> T; } -impl NonsymmetricConeUtils for C +// -------------------------------------- +// Useful utilities for nonsymmetric cones +// ------------------------------------- + +impl NonsymmetricCone where T: FloatT, - C: NonsymmetricCone, { // find the maximum step length α≥0 so that // q + α*dq stays in an exponential or power // cone, or their respective dual cones. - fn step_length_n_cone( - & self, + fn step_length_cone( + &self, wq: &mut [T], dq: &[T], q: &[T], @@ -266,4 +210,4 @@ where } α } -} \ No newline at end of file +} diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs index 45c79604..bd28cd2b 100644 --- a/src/solver/core/cones/genpowcone.rs +++ b/src/solver/core/cones/genpowcone.rs @@ -3,51 +3,52 @@ use crate::{ algebra::*, solver::{core::ScalingStrategy, CoreSettings}, }; +use std::iter::zip; // ------------------------------------- // Generalized Power Cone // ------------------------------------- pub struct GenPowerCone { - // power defining the cone + // power defining the cone. length determines dim1 α: Vec, + // dimension of w + dim2: usize, + // gradient of the dual barrier at z grad: Vec, // holds copy of z at scaling point z: Vec, - // dimension of u, w and their sum - pub dim1: usize, - pub dim2: usize, - dim: usize, // central path parameter - pub μ: T, + μ: T, + // vectors for rank 3 update representation of Hs - pub p: Vec, - pub q: Vec, - pub r: Vec, + p: Vec, + q: Vec, + r: Vec, // first part of the diagonal d1: Vec, + // additional scalar terms for rank-2 rep d2: T, // additional constant for initialization in the Newton-Raphson method ψ: T, + + //work vector length dim, e.g. for line searches + work: Vec, } impl GenPowerCone where T: FloatT, { - pub fn new(α: Vec, dim1: usize, dim2: usize) -> Self { + pub fn new(α: Vec, dim2: usize) -> Self { + let dim1 = α.len(); let dim = dim1 + dim2; - //PJG : this check belongs elsewhere + //PJG : these check belongs elsewhere assert!(α.iter().all(|r| *r > T::zero())); // check all powers are greater than 0 - - // YC: should polish this check - let mut powerr = T::zero(); - α.iter().for_each(|x| powerr += *x); - powerr -= T::one(); - assert!(powerr.abs() < T::epsilon()); //check the sum of powers is 1 + assert!((T::one() - α.sum()).abs() < T::epsilon()); let ψ = T::one() / (α.sumsq()); @@ -55,9 +56,7 @@ where α, grad: vec![T::zero(); dim], z: vec![T::zero(); dim], - dim1, dim2, - dim, μ: T::one(), p: vec![T::zero(); dim], q: vec![T::zero(); dim1], @@ -65,8 +64,19 @@ where d1: vec![T::zero(); dim1], d2: T::zero(), ψ, + work: [T::zero(); dim], } } + + pub fn dim1(&self) -> usize { + self.α.len() + } + pub fn dim2(&self) -> usize { + self.dim2 + } + pub fn dim(&self) -> usize { + self.dim1() + self.dim2() + } } impl Cone for GenPowerCone @@ -74,11 +84,11 @@ where T: FloatT, { fn degree(&self) -> usize { - self.dim1 + 1 + self.dim1() + 1 } fn numel(&self) -> usize { - self.dim + self.dim() } fn is_symmetric(&self) -> bool { @@ -104,11 +114,11 @@ where fn unit_initialization(&self, z: &mut [T], s: &mut [T]) { let α = &self.α; - s[..α.len()] + s[..self.dim1()] .iter_mut() .enumerate() .for_each(|(i, s)| *s = (T::one() + α[i]).sqrt()); - s[α.len()..].iter_mut().for_each(|x| *x = T::zero()); + s[self.dim1()..].iter_mut().for_each(|x| *x = T::zero()); z.copy_from(&s); } @@ -142,40 +152,34 @@ where fn get_Hs(&self, Hsblock: &mut [T]) { // we are returning here the diagonal D = [d1; d2] block - let d1 = &self.d1; - Hsblock[..self.dim1] - .iter_mut() - .enumerate() - .for_each(|(i, x)| *x = self.μ * d1[i]); - let c = self.μ * self.d2; - Hsblock[self.dim1..].iter_mut().for_each(|x| *x = c); + let dim1 = self.dim1(); + + Hsblock[..dim1].scalarop_from(|x| self.μ * x, &self.d1); + Hsblock[dim1..].set(self.μ * self.d2); } fn mul_Hs(&mut self, y: &mut [T], x: &[T], _work: &mut [T]) { - let d1 = &self.d1; - let d2 = self.d2; - let dim1 = self.dim1; + // Hs = μ*(D + pp' -qq' -rr') + + let rng1 = 0..self.dim1(); + let rng2 = self.dim1()..self.dim(); let coef_p = self.p.dot(x); - let coef_q = self.q.dot(&x[..dim1]); - let coef_r = self.r.dot(&x[dim1..]); + let coef_q = self.q.dot(&x[rng1]); + let coef_r = self.r.dot(&x[rng2]); - let x1 = &x[..dim1]; - let x2 = &x[dim1..]; + // y1 .= K.d1 .* x1 - coef_q.*K.q + // NB: d1 is a vector + zip(zip(&mut y[rng1], &x[rng1]), zip(&self.d1, &self.q)) + .for_each(|((y, &x), (&d1, &q))| *y += d1 * x - coef_q * q); - y.iter_mut() - .enumerate() - .for_each(|(i, x)| *x = coef_p * self.p[i]); - y[..dim1] - .iter_mut() - .enumerate() - .for_each(|(i, x)| *x += d1[i] * x1[i] - coef_q * self.q[i]); - y[dim1..] - .iter_mut() - .enumerate() - .skip(dim1) - .for_each(|(i, x)| *x += d2 * x2[i] - coef_r * self.r[i]); - y.iter_mut().for_each(|x| *x *= self.μ); + // y2 .= K.d2 .* x2 - coef_r.*K.r. + // NB: d2 is a scalar + zip(zip(&mut y[rng2], &x[rng2]), &self.r) + .for_each(|((y, &x), &r)| *y += self.d2 * x - coef_r * r); + + y.axpby(coef_p, &self.p, T::one()); + y.scale(self.μ); } fn affine_ds(&self, ds: &mut [T], s: &[T]) { @@ -186,12 +190,7 @@ where &mut self, shift: &mut [T], _step_z: &mut [T], _step_s: &mut [T], σμ: T ) { //YC: No 3rd order correction at present - - let grad = &self.grad; - shift - .iter_mut() - .enumerate() - .for_each(|(i, x)| *x = σμ * grad[i]); + shift.scalarop_from(|g| g * σμ, &self.grad); } fn Δs_from_Δz_offset(&mut self, out: &mut [T], ds: &[T], _work: &mut [T], _z: &[T]) { @@ -211,15 +210,27 @@ where let αmin = settings.min_terminate_step_length; // final backtracked position - let _is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; let _is_dual_feasible_fcn = |s: &[T]| -> bool { self.is_dual_feasible(s) }; - //YC: Do we need to care about the memory allocation time for wq? - let mut wq = Vec::with_capacity(self.dim); - - let αz = self.step_length_n_cone(&mut wq, dz, z, αmax, αmin, step, _is_dual_feasible_fcn); - let αs = self.step_length_n_cone(&mut wq, ds, s, αmax, αmin, step, _is_prim_feasible_fcn); + let αz = self.step_length_cone( + &mut self.work, + dz, + z, + αmax, + αmin, + step, + _is_dual_feasible_fcn, + ); + let αs = self.step_length_cone( + &mut self.work, + ds, + s, + αmax, + αmin, + step, + _is_prim_feasible_fcn, + ); (αz, αs) } @@ -227,18 +238,17 @@ where fn compute_barrier(&self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { let mut barrier = T::zero(); - //YC: Do we need to care about the memory allocation time for wq? - let mut wq = Vec::with_capacity(self.dim); + let wq = &mut self.work; wq.iter_mut() .enumerate() .for_each(|(i, x)| *x = z[i] + α * dz[i]); - barrier += self.barrier_dual(&wq); + barrier += self.barrier_dual(wq); wq.iter_mut() .enumerate() .for_each(|(i, x)| *x = s[i] + α * ds[i]); - barrier += self.barrier_primal(&wq); + barrier += self.barrier_primal(wq); barrier } From 5983b70effdfa1429c8dccce538950e005c7f7e5 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Mon, 4 Sep 2023 20:01:28 +0100 Subject: [PATCH 17/52] reorganize nonsym cone traits --- src/solver/core/cones/compositecone.rs | 4 +- src/solver/core/cones/expcone.rs | 46 ++--- src/solver/core/cones/exppow_common.rs | 130 +++++++------- src/solver/core/cones/genpowcone.rs | 158 +++++++++--------- src/solver/core/cones/mod.rs | 8 +- src/solver/core/cones/nonnegativecone.rs | 2 +- src/solver/core/cones/powcone.rs | 74 ++++---- src/solver/core/cones/socone.rs | 3 +- src/solver/core/cones/supportedcone.rs | 11 +- src/solver/core/cones/zerocone.rs | 2 +- .../kktsolvers/direct/quasidef/datamap.rs | 12 +- .../direct/quasidef/directldlkktsolver.rs | 8 +- .../core/kktsolvers/direct/quasidef/utils.rs | 16 +- src/solver/core/solver.rs | 6 +- src/solver/core/traits.rs | 4 +- .../implementations/default/variables.rs | 2 +- 16 files changed, 242 insertions(+), 244 deletions(-) diff --git a/src/solver/core/cones/compositecone.rs b/src/solver/core/cones/compositecone.rs index a85f7fb0..e793504e 100644 --- a/src/solver/core/cones/compositecone.rs +++ b/src/solver/core/cones/compositecone.rs @@ -348,9 +348,9 @@ where (α, α) } - fn compute_barrier(&self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { + fn compute_barrier(&mut self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { let mut barrier = T::zero(); - for (cone, rng) in zip(&self.cones, &self.rng_cones) { + for (cone, rng) in zip(&mut self.cones, &self.rng_cones) { let zi = &z[rng.clone()]; let si = &s[rng.clone()]; let dzi = &dz[rng.clone()]; diff --git a/src/solver/core/cones/expcone.rs b/src/solver/core/cones/expcone.rs index 958c6b29..473ad9c3 100644 --- a/src/solver/core/cones/expcone.rs +++ b/src/solver/core/cones/expcone.rs @@ -147,19 +147,18 @@ where let step = settings.linesearch_backtrack_step; let αmin = settings.min_terminate_step_length; - // final backtracked position - let wq = &mut [T::zero(); 3]; + let mut work = [T::zero(); 3]; let _is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; let _is_dual_feasible_fcn = |s: &[T]| -> bool { self.is_dual_feasible(s) }; - let αz = self.step_length_cone(wq, dz, z, αmax, αmin, step, _is_dual_feasible_fcn); - let αs = self.step_length_cone(wq, ds, s, αmax, αmin, step, _is_prim_feasible_fcn); + let αz = Self::backtrack_search(dz, z, αmax, αmin, step, _is_dual_feasible_fcn, &mut work); + let αs = Self::backtrack_search(ds, s, αmax, αmin, step, _is_prim_feasible_fcn, &mut work); (αz, αs) } - fn compute_barrier(&self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { + fn compute_barrier(&mut self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { let mut barrier = T::zero(); let cur_z = [z[0] + α * dz[0], z[1] + α * dz[1], z[2] + α * dz[2]]; @@ -172,9 +171,7 @@ where } } -// implement this marker trait to get access to -// functions common to exp and pow cones -impl Nonsymmetric3DCone for ExponentialCone +impl NonsymmetricCone for ExponentialCone where T: FloatT, { @@ -214,20 +211,6 @@ where false } - // Compute the primal gradient of f(s) at s - fn gradient_primal(&self, s: &[T]) -> [T; 3] - where - T: FloatT, - { - let mut g = [T::zero(); 3]; - let ω = _wright_omega(T::one() - s[0] / s[1] - (s[1] / s[2]).logsafe()); - - g[0] = T::one() / ((ω - T::one()) * s[1]); - g[1] = g[0] + g[0] * ((ω * s[1] / s[2]).logsafe()) - T::one() / s[1]; - g[2] = ω / ((T::one() - ω) * s[2]); - g - } - fn update_dual_grad_H(&mut self, z: &[T]) { let grad = &mut self.grad; let H = &mut self.H_dual; @@ -354,6 +337,25 @@ where η[..].scale((0.5).as_T()); } +} + +impl Nonsymmetric3DCone for ExponentialCone +where + T: FloatT, +{ + // Compute the primal gradient of f(s) at s + fn gradient_primal(&self, s: &[T]) -> [T; 3] + where + T: FloatT, + { + let mut g = [T::zero(); 3]; + let ω = _wright_omega(T::one() - s[0] / s[1] - (s[1] / s[2]).logsafe()); + + g[0] = T::one() / ((ω - T::one()) * s[1]); + g[1] = g[0] + g[0] * ((ω * s[1] / s[2]).logsafe()) - T::one() / s[1]; + g[2] = ω / ((T::one() - ω) * s[2]); + g + } //getters fn split_borrow_mut( diff --git a/src/solver/core/cones/exppow_common.rs b/src/solver/core/cones/exppow_common.rs index 21b81e2e..b38b5247 100644 --- a/src/solver/core/cones/exppow_common.rs +++ b/src/solver/core/cones/exppow_common.rs @@ -20,23 +20,63 @@ pub(crate) trait NonsymmetricCone { fn higher_correction(&mut self, η: &mut [T; 3], ds: &[T], v: &[T]); } +#[allow(clippy::too_many_arguments)] +pub(crate) trait NonsymmetricConeUtils { + fn backtrack_search( + dq: &[T], + q: &[T], + α_init: T, + α_min: T, + backtrack: T, + is_in_cone_fcn: impl Fn(&[T]) -> bool, + work: &mut [T], + ) -> T; +} + +impl NonsymmetricConeUtils for C +where + T: FloatT, + C: NonsymmetricCone, +{ + // find the maximum step length α≥0 so that + // q + α*dq stays in an exponential or power + // cone, or their respective dual cones. + fn backtrack_search( + dq: &[T], + q: &[T], + α_init: T, + α_min: T, + backtrack: T, + is_in_cone_fcn: impl Fn(&[T]) -> bool, + work: &mut [T], + ) -> T { + let mut α = α_init; + + loop { + // work = q + α*dq + work.waxpby(T::one(), &q, α, &dq); + + if is_in_cone_fcn(work) { + break; + } + α *= backtrack; + if α < α_min { + α = T::zero(); + break; + } + } + α + } +} + // -------------------------------------- -// Traits and blanket implementations for Exponential, 3D Power Cones +// Trait and blanket utlity implementations for Exponential and 3D Power Cones // ------------------------------------- #[allow(clippy::too_many_arguments)] -pub(crate) trait Nonsymmetric3DCone { - fn update_Hs(&mut self, s: &[T], z: &[T], μ: T, scaling_strategy: ScalingStrategy); - // Compute the primal gradient of f(s) at s +pub(crate) trait Nonsymmetric3DCone { fn gradient_primal(&self, s: &[T]) -> [T; 3]; - fn use_dual_scaling(&mut self, μ: T); - - fn use_primal_dual_scaling(&mut self, s: &[T], z: &[T]); - - // we can't mutably borrow individual fields through getter methods, - // so we have this one method to borrow them simultaneously. - // Usage: let (H_dual, Hs, grad, z) = self.split_borrow_mut (); fn split_borrow_mut( &mut self, ) -> ( @@ -47,7 +87,15 @@ pub(crate) trait Nonsymmetric3DCone { ); } -impl Nonsymmetric3DCone for C +pub(crate) trait Nonsymmetric3DConeUtils { + fn update_Hs(&mut self, s: &[T], z: &[T], μ: T, scaling_strategy: ScalingStrategy); + + fn use_dual_scaling(&mut self, μ: T); + + fn use_primal_dual_scaling(&mut self, s: &[T], z: &[T]); +} + +impl Nonsymmetric3DConeUtils for C where T: FloatT, C: Nonsymmetric3DCone, @@ -145,7 +193,7 @@ where } // -------------------------------------- -// Traits Generalized PowerCones +// Traits for general ND cones // ------------------------------------- // Operations supported on ND nonsymmetrics only. Note this @@ -155,59 +203,3 @@ pub(crate) trait NonsymmetricNDCone { // Compute the primal gradient of f(s) at s fn minus_gradient_primal(&self, s: &[T]) -> (T, T); } - -#[allow(clippy::too_many_arguments)] -pub(crate) trait NonsymmetricConeUtils { - fn step_length_cone( - &self, - wq: &mut [T], - dq: &[T], - q: &[T], - α_init: T, - α_min: T, - backtrack: T, - is_in_cone_fcn: impl Fn(&[T]) -> bool, - ) -> T; -} - -// -------------------------------------- -// Useful utilities for nonsymmetric cones -// ------------------------------------- - -impl NonsymmetricCone -where - T: FloatT, -{ - // find the maximum step length α≥0 so that - // q + α*dq stays in an exponential or power - // cone, or their respective dual cones. - fn step_length_cone( - &self, - wq: &mut [T], - dq: &[T], - q: &[T], - α_init: T, - α_min: T, - backtrack: T, - is_in_cone_fcn: impl Fn(&[T]) -> bool, - ) -> T { - let mut α = α_init; - - loop { - // wq = q + α*dq - for i in 0..q.len() { - wq[i] = q[i] + α * dq[i]; - } - - if is_in_cone_fcn(wq) { - break; - } - α *= backtrack; - if α < α_min { - α = T::zero(); - break; - } - } - α - } -} diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs index bd28cd2b..6a798c8f 100644 --- a/src/solver/core/cones/genpowcone.rs +++ b/src/solver/core/cones/genpowcone.rs @@ -19,15 +19,15 @@ pub struct GenPowerCone { grad: Vec, // holds copy of z at scaling point z: Vec, + // central path parameter - μ: T, + pub μ: T, // vectors for rank 3 update representation of Hs - p: Vec, - q: Vec, - r: Vec, - // first part of the diagonal - d1: Vec, + pub p: Vec, + pub q: Vec, + pub r: Vec, + pub d1: Vec, // additional scalar terms for rank-2 rep d2: T, @@ -64,7 +64,7 @@ where d1: vec![T::zero(); dim1], d2: T::zero(), ψ, - work: [T::zero(); dim], + work: vec![T::zero(); dim], } } @@ -161,22 +161,26 @@ where fn mul_Hs(&mut self, y: &mut [T], x: &[T], _work: &mut [T]) { // Hs = μ*(D + pp' -qq' -rr') - let rng1 = 0..self.dim1(); - let rng2 = self.dim1()..self.dim(); + let dim1 = self.dim1(); let coef_p = self.p.dot(x); - let coef_q = self.q.dot(&x[rng1]); - let coef_r = self.r.dot(&x[rng2]); + let coef_q = self.q.dot(&x[..dim1]); + let coef_r = self.r.dot(&x[dim1..]); // y1 .= K.d1 .* x1 - coef_q.*K.q // NB: d1 is a vector - zip(zip(&mut y[rng1], &x[rng1]), zip(&self.d1, &self.q)) + let y1 = &mut y[..dim1]; + let x1 = &x[..dim1]; + + zip(zip(y1, x1), zip(&self.d1, &self.q)) .for_each(|((y, &x), (&d1, &q))| *y += d1 * x - coef_q * q); // y2 .= K.d2 .* x2 - coef_r.*K.r. // NB: d2 is a scalar - zip(zip(&mut y[rng2], &x[rng2]), &self.r) - .for_each(|((y, &x), &r)| *y += self.d2 * x - coef_r * r); + let y2 = &mut y[dim1..]; + let x2 = &x[dim1..]; + + zip(zip(y2, x2), &self.r).for_each(|((y, &x), &r)| *y += self.d2 * x - coef_r * r); y.axpby(coef_p, &self.p, T::one()); y.scale(self.μ); @@ -209,46 +213,35 @@ where let step = settings.linesearch_backtrack_step; let αmin = settings.min_terminate_step_length; - // final backtracked position + //simultaneously using "work" and the closures defined + //below produces a borrow check error, so temporarily + //move "work" out of self + let mut work = std::mem::take(&mut self.work); + let _is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; let _is_dual_feasible_fcn = |s: &[T]| -> bool { self.is_dual_feasible(s) }; - let αz = self.step_length_cone( - &mut self.work, - dz, - z, - αmax, - αmin, - step, - _is_dual_feasible_fcn, - ); - let αs = self.step_length_cone( - &mut self.work, - ds, - s, - αmax, - αmin, - step, - _is_prim_feasible_fcn, - ); + let αz = Self::backtrack_search(dz, z, αmax, αmin, step, _is_dual_feasible_fcn, &mut work); + let αs = Self::backtrack_search(ds, s, αmax, αmin, step, _is_prim_feasible_fcn, &mut work); + + //restore work to self + self.work = work; (αz, αs) } - fn compute_barrier(&self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { + fn compute_barrier(&mut self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { let mut barrier = T::zero(); - let wq = &mut self.work; + let mut work = std::mem::take(&mut self.work); - wq.iter_mut() - .enumerate() - .for_each(|(i, x)| *x = z[i] + α * dz[i]); - barrier += self.barrier_dual(wq); + work.waxpby(T::one(), &z, α, &dz); + barrier += self.barrier_dual(&work); - wq.iter_mut() - .enumerate() - .for_each(|(i, x)| *x = s[i] + α * ds[i]); - barrier += self.barrier_primal(wq); + work.waxpby(T::one(), &s, α, &ds); + barrier += self.barrier_primal(&work); + + self.work = work; barrier } @@ -258,7 +251,6 @@ where // Dual scaling //------------------------------------- -//YC: better to define an additional trait "NonsymmetricCone" for it impl NonsymmetricCone for GenPowerCone where T: FloatT, @@ -270,7 +262,7 @@ where { let α = &self.α; let two: T = (2f64).as_T(); - let dim1 = self.dim1; + let dim1 = self.dim1(); if s[..dim1].iter().all(|x| *x > T::zero()) { let mut res = T::zero(); @@ -293,7 +285,7 @@ where { let α = &self.α; let two: T = (2.).as_T(); - let dim1 = self.dim1; + let dim1 = self.dim1(); if z[..dim1].iter().all(|x| *x > T::zero()) { let mut res = T::zero(); @@ -309,37 +301,9 @@ where false } - // Compute the primal gradient of f(s) at s - fn minus_gradient_primal(&self, s: &[T]) -> (T, T) - where - T: FloatT, - { - let α = &self.α; - let dim1 = self.dim1; - let two: T = (2.).as_T(); - - // unscaled phi - let mut phi = T::one(); - for i in 0..dim1 { - phi *= s[i].powf(two * α[i]); - } - - // obtain g1 from the Newton-Raphson method - let norm_r = s[dim1..].norm(); - let mut g1 = T::zero(); - - if norm_r > T::epsilon() { - g1 = _newton_raphson_genpowcone(norm_r, &s[..dim1], phi, α, self.ψ); - // minus_g.iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x = g1*s[i]/norm_r); - // minus_g[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x = -(T::one()+α[i]+α[i]*g1*norm_r)/s[i]); - } - - (g1, norm_r) - } - fn update_dual_grad_H(&mut self, z: &[T]) { let α = &self.α; - let dim1 = self.dim1; + let dim1 = self.dim1(); let two: T = (2.).as_T(); let mut phi = T::one(); @@ -384,7 +348,7 @@ where p[..dim1].copy_from(&τ); p[..dim1].iter_mut().for_each(|x| *x *= c1); let c2 = p1 / ζ; - p[dim1..].copy_from(&z[self.dim1..]); + p[dim1..].copy_from(&z[dim1..]); p[dim1..].iter_mut().for_each(|x| *x *= c2); let c3 = q0 / ζ; @@ -402,7 +366,7 @@ where { // Dual barrier: let α = &self.α; - let dim1 = self.dim1; + let dim1 = self.dim1(); let two: T = (2.).as_T(); let mut res = T::zero(); @@ -430,9 +394,9 @@ where //YC: Do we need to care about the memory allocation time for minus_q? let (g1, norm_r) = self.minus_gradient_primal(s); - let mut minus_g = Vec::with_capacity(self.dim); + let mut minus_g = Vec::with_capacity(self.dim()); - let dim1 = self.dim1; + let dim1 = self.dim1(); if norm_r > T::epsilon() { minus_g .iter_mut() @@ -458,8 +422,44 @@ where out } + + fn higher_correction(&mut self, _η: &mut [T; 3], _ds: &[T], _v: &[T]) { + unimplemented!() + } } +impl NonsymmetricNDCone for GenPowerCone +where + T: FloatT, +{ + // Compute the primal gradient of f(s) at s + fn minus_gradient_primal(&self, s: &[T]) -> (T, T) + where + T: FloatT, + { + let α = &self.α; + let dim1 = self.dim1(); + let two: T = (2.).as_T(); + + // unscaled phi + let mut phi = T::one(); + for i in 0..dim1 { + phi *= s[i].powf(two * α[i]); + } + + // obtain g1 from the Newton-Raphson method + let norm_r = s[dim1..].norm(); + let mut g1 = T::zero(); + + if norm_r > T::epsilon() { + g1 = _newton_raphson_genpowcone(norm_r, &s[..dim1], phi, α, self.ψ); + // minus_g.iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x = g1*s[i]/norm_r); + // minus_g[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x = -(T::one()+α[i]+α[i]*g1*norm_r)/s[i]); + } + + (g1, norm_r) + } +} // ---------------------------------------------- // internal operations for generalized power cones diff --git a/src/solver/core/cones/mod.rs b/src/solver/core/cones/mod.rs index f7833da3..311cc77f 100644 --- a/src/solver/core/cones/mod.rs +++ b/src/solver/core/cones/mod.rs @@ -10,11 +10,11 @@ mod compositecone; mod supportedcone; // primitive cone types mod expcone; +mod genpowcone; mod nonnegativecone; mod powcone; mod socone; mod zerocone; -mod genpowcone; // partially specialized traits and blanket implementataions mod exppow_common; mod symmetric_common; @@ -22,8 +22,8 @@ mod symmetric_common; //re-export everything to appear as one module use exppow_common::*; pub use { - compositecone::*, expcone::*, nonnegativecone::*, powcone::*, socone::*, supportedcone::*, - symmetric_common::*, zerocone::*, genpowcone::*, + compositecone::*, expcone::*, genpowcone::*, nonnegativecone::*, powcone::*, socone::*, + supportedcone::*, symmetric_common::*, zerocone::*, }; // only use PSD cones with SDP/Blas enabled @@ -143,5 +143,5 @@ where ) -> (T, T); // return the barrier function at (z+αdz,s+αds) - fn compute_barrier(&self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T; + fn compute_barrier(&mut self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T; } diff --git a/src/solver/core/cones/nonnegativecone.rs b/src/solver/core/cones/nonnegativecone.rs index 55bb4f8d..d8665a9c 100644 --- a/src/solver/core/cones/nonnegativecone.rs +++ b/src/solver/core/cones/nonnegativecone.rs @@ -146,7 +146,7 @@ where (αz, αs) } - fn compute_barrier(&self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { + fn compute_barrier(&mut self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { assert_eq!(z.len(), s.len()); assert_eq!(dz.len(), z.len()); assert_eq!(ds.len(), s.len()); diff --git a/src/solver/core/cones/powcone.rs b/src/solver/core/cones/powcone.rs index 59a92a60..a1d867de 100644 --- a/src/solver/core/cones/powcone.rs +++ b/src/solver/core/cones/powcone.rs @@ -150,19 +150,18 @@ where let step = settings.linesearch_backtrack_step; let αmin = settings.min_terminate_step_length; - // final backtracked position - let mut wq = [T::zero(); 3]; + let mut work = [T::zero(); 3]; let _is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; let _is_dual_feasible_fcn = |s: &[T]| -> bool { self.is_dual_feasible(s) }; - let αz = self.step_length_3d_cone(&mut wq, dz, z, αmax, αmin, step, _is_dual_feasible_fcn); - let αs = self.step_length_3d_cone(&mut wq, ds, s, αmax, αmin, step, _is_prim_feasible_fcn); + let αz = Self::backtrack_search(dz, z, αmax, αmin, step, _is_dual_feasible_fcn, &mut work); + let αs = Self::backtrack_search(ds, s, αmax, αmin, step, _is_prim_feasible_fcn, &mut work); (αz, αs) } - fn compute_barrier(&self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { + fn compute_barrier(&mut self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { let mut barrier = T::zero(); let cur_z = [z[0] + α * dz[0], z[1] + α * dz[1], z[2] + α * dz[2]]; @@ -179,7 +178,7 @@ where // primal-dual scaling //------------------------------------- -impl Nonsymmetric3DCone for PowerCone +impl NonsymmetricCone for PowerCone where T: FloatT, { @@ -220,35 +219,6 @@ where false } - // Compute the primal gradient of f(s) at s - fn gradient_primal(&self, s: &[T]) -> [T; 3] - where - T: FloatT, - { - let α = self.α; - let mut g = [T::zero(); 3]; - let two: T = (2.).as_T(); - - // unscaled ϕ - let phi = (s[0]).powf(two * α) * (s[1]).powf(two - α * two); - - // obtain last element of g from the Newton-Raphson method - let abs_s = s[2].abs(); - if abs_s > T::epsilon() { - g[2] = _newton_raphson_powcone(abs_s, phi, α); - if s[2] < T::zero() { - g[2] = -g[2]; - } - g[0] = -(α * g[2] * s[2] + T::one() + α) / s[0]; - g[1] = -((T::one() - α) * g[2] * s[2] + two - α) / s[1]; - } else { - g[2] = T::zero(); - g[0] = -(T::one() + α) / s[0]; - g[1] = -(two - α) / s[1]; - } - g - } - fn update_dual_grad_H(&mut self, z: &[T]) { let H = &mut self.H_dual; let α = self.α; @@ -410,6 +380,40 @@ where η[..].axpby(dotψv * inv_ψ2, Hψu, T::one()); η[..].scale((0.5).as_T()); } +} + +impl Nonsymmetric3DCone for PowerCone +where + T: FloatT, +{ + // Compute the primal gradient of f(s) at s + fn gradient_primal(&self, s: &[T]) -> [T; 3] + where + T: FloatT, + { + let α = self.α; + let mut g = [T::zero(); 3]; + let two: T = (2.).as_T(); + + // unscaled ϕ + let phi = (s[0]).powf(two * α) * (s[1]).powf(two - α * two); + + // obtain last element of g from the Newton-Raphson method + let abs_s = s[2].abs(); + if abs_s > T::epsilon() { + g[2] = _newton_raphson_powcone(abs_s, phi, α); + if s[2] < T::zero() { + g[2] = -g[2]; + } + g[0] = -(α * g[2] * s[2] + T::one() + α) / s[0]; + g[1] = -((T::one() - α) * g[2] * s[2] + two - α) / s[1]; + } else { + g[2] = T::zero(); + g[0] = -(T::one() + α) / s[0]; + g[1] = -(two - α) / s[1]; + } + g + } //getters fn split_borrow_mut( diff --git a/src/solver/core/cones/socone.rs b/src/solver/core/cones/socone.rs index fbe4617c..4999d9de 100644 --- a/src/solver/core/cones/socone.rs +++ b/src/solver/core/cones/socone.rs @@ -201,7 +201,7 @@ where (αz, αs) } - fn compute_barrier(&self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { + fn compute_barrier(&mut self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { let res_s = _soc_residual_shifted(s, ds, α); let res_z = _soc_residual_shifted(z, dz, α); @@ -390,6 +390,7 @@ where // fcn. The operation λ = Wz produces a borrow conflict // otherwise because λ is part of the cone's internal data // and we can't borrow self and &mut λ at the same time. +// Could also have been done using std::mem::take #[allow(non_snake_case)] fn _soc_mul_W_inner(y: &mut [T], x: &[T], α: T, β: T, w: &[T], η: T) diff --git a/src/solver/core/cones/supportedcone.rs b/src/solver/core/cones/supportedcone.rs index c4168ea0..21646324 100644 --- a/src/solver/core/cones/supportedcone.rs +++ b/src/solver/core/cones/supportedcone.rs @@ -32,8 +32,7 @@ pub enum SupportedConeT { /// The generalized power cone. /// /// The parameter indicates the power and dimensions. - /// PJG: second dimensions is redundant to length(α) - GenPowerConeT(Vec, usize, usize), + GenPowerConeT(Vec, usize), /// The exponential cone in R^3. /// /// This cone takes no parameters @@ -60,7 +59,7 @@ impl SupportedConeT { SupportedConeT::PowerConeT(_) => 3, #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(dim) => triangular_number(*dim), - SupportedConeT::GenPowerConeT(_, dim1, dim2) => *dim1 + *dim2, + SupportedConeT::GenPowerConeT(α, dim2) => α.len() + *dim2, } } } @@ -87,8 +86,8 @@ pub fn make_cone(cone: &SupportedConeT) -> SupportedCone { SupportedConeT::PowerConeT(α) => PowerCone::::new(*α).into(), #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(dim) => PSDTriangleCone::::new(dim).into(), - SupportedConeT::GenPowerConeT(α, dim1, dim2) => { - GenPowerCone::::new((*α).clone(), *dim1, *dim2).into() + SupportedConeT::GenPowerConeT(α, dim2) => { + GenPowerCone::::new((*α).clone(), *dim2).into() } } } @@ -159,7 +158,7 @@ impl SupportedConeAsTag for SupportedConeT { SupportedConeT::PowerConeT(_) => SupportedConeTag::PowerCone, #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(_) => SupportedConeTag::PSDTriangleCone, - SupportedConeT::GenPowerConeT(_, _, _) => SupportedConeTag::GenPowerCone, + SupportedConeT::GenPowerConeT(_, _) => SupportedConeTag::GenPowerCone, } } } diff --git a/src/solver/core/cones/zerocone.rs b/src/solver/core/cones/zerocone.rs index 6dc2eceb..281c16f7 100644 --- a/src/solver/core/cones/zerocone.rs +++ b/src/solver/core/cones/zerocone.rs @@ -121,7 +121,7 @@ where (αmax, αmax) } - fn compute_barrier(&self, _z: &[T], _s: &[T], _dz: &[T], _ds: &[T], _α: T) -> T { + fn compute_barrier(&mut self, _z: &[T], _s: &[T], _dz: &[T], _ds: &[T], _α: T) -> T { T::zero() } } diff --git a/src/solver/core/kktsolvers/direct/quasidef/datamap.rs b/src/solver/core/kktsolvers/direct/quasidef/datamap.rs index 8dfa42b7..a3ab149e 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/datamap.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/datamap.rs @@ -12,10 +12,10 @@ pub struct LDLDataMap { pub SOC_u: Vec>, //off diag dense columns u pub SOC_v: Vec>, //off diag dense columns v pub SOC_D: Vec, //diag of just the sparse SOC expansion D - pub GenPow_p: Vec>, // off diag dense columns p - pub GenPow_q: Vec>, // off diag dense columns q - pub GenPow_r: Vec>, // off diag dense columns r - pub GenPow_D: Vec, // diag of just the sparse GenPow expansion D + pub GenPow_p: Vec>, // off diag dense columns p + pub GenPow_q: Vec>, // off diag dense columns q + pub GenPow_r: Vec>, // off diag dense columns r + pub GenPow_D: Vec, // diag of just the sparse GenPow expansion D // all of above terms should be disjoint and their union // should cover all of the user data in the KKT matrix. Now @@ -70,8 +70,8 @@ impl LDLDataMap { // Generalized power cones if let SupportedCone::GenPowerCone(genpow) = cone { GenPow_p.push(vec![0; genpow.numel()]); - GenPow_q.push(vec![0; genpow.dim1]); - GenPow_r.push(vec![0; genpow.dim2]); + GenPow_q.push(vec![0; genpow.dim1()]); + GenPow_r.push(vec![0; genpow.dim2()]); } } diff --git a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs index aaf1f999..d7d00949 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs @@ -458,8 +458,8 @@ fn _fill_signs(signs: &mut [i8], m: usize, n: usize, p_soc: usize, p_genpow: usi .step_by(3) .for_each(|x| *x = -*x); signs[(n + m + p_soc)..(n + m + p_soc + p_genpow)] - .iter_mut() - .skip(1) - .step_by(3) - .for_each(|x| *x = -*x); + .iter_mut() + .skip(1) + .step_by(3) + .for_each(|x| *x = -*x); } diff --git a/src/solver/core/kktsolvers/direct/quasidef/utils.rs b/src/solver/core/kktsolvers/direct/quasidef/utils.rs index 3d2707a7..01a2e6cd 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/utils.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/utils.rs @@ -125,9 +125,9 @@ fn _kkt_assemble_colcounts( let mut socidx = 0; // which SOC are we working on? for (i, cone) in cones.iter().enumerate() { - if let SupportedCone::SecondOrderCone(SOC) = cone { + if let SupportedCone::SecondOrderCone(socone) = cone { // we will add the u and v columns for this cone - let nvars = SOC.numel(); + let nvars = socone.numel(); let headidx = cones.rng_cones[i].start; // which column does u go into? @@ -155,11 +155,11 @@ fn _kkt_assemble_colcounts( let mut genpowidx = 0; // which Genpow are we working on? for (i, cone) in cones.iter().enumerate() { - if let SupportedCone::GenPowerCone(GenPow) = cone { + if let SupportedCone::GenPowerCone(gpcone) = cone { // we will add the p,q,r columns for this cone - let nvars = GenPow.numel(); - let dim1 = GenPow.dim1; - let dim2 = GenPow.dim2; + let nvars = gpcone.numel(); + let dim1 = gpcone.dim1(); + let dim2 = gpcone.dim2(); let headidx = cones.rng_cones[i].start; // which column does q go into? @@ -263,9 +263,9 @@ fn _kkt_assemble_fill( let mut genpowidx = 0; //which generalized power cone are we working on? for (i, cone) in cones.iter().enumerate() { - if let SupportedCone::GenPowerCone(cone) = cone { + if let SupportedCone::GenPowerCone(gpcone) = cone { let headidx = cones.rng_cones[i].start; - let dim1 = cone.dim1; + let dim1 = gpcone.dim1(); // which column does q go into (if triu)? let col = m + n + 2 * socidx + 3 * genpowidx; diff --git a/src/solver/core/solver.rs b/src/solver/core/solver.rs index 36c826ce..5486e0df 100644 --- a/src/solver/core/solver.rs +++ b/src/solver/core/solver.rs @@ -386,7 +386,7 @@ mod internal { -> T; /// backtrack a step direction to the barrier - fn backtrack_step_to_barrier(&self, αinit: T) -> T; + fn backtrack_step_to_barrier(&mut self, αinit: T) -> T; /// Scaling strategy checkpointing functions fn strategy_checkpoint_insufficient_progress( @@ -472,12 +472,12 @@ mod internal { α } - fn backtrack_step_to_barrier(&self, αinit: T) -> T { + fn backtrack_step_to_barrier(&mut self, αinit: T) -> T { let backtrack = self.settings.core().linesearch_backtrack_step; let mut α = αinit; for _ in 0..50 { - let barrier = self.variables.barrier(&self.step_lhs, α, &self.cones); + let barrier = self.variables.barrier(&self.step_lhs, α, &mut self.cones); if barrier < T::one() { return α; } else { diff --git a/src/solver/core/traits.rs b/src/solver/core/traits.rs index f7bdeee4..3131867a 100644 --- a/src/solver/core/traits.rs +++ b/src/solver/core/traits.rs @@ -79,13 +79,13 @@ pub trait Variables { /// Overwrite values with those from another object fn copy_from(&mut self, src: &Self); - /// Apply NT scaling to the a collection of cones. + /// Apply NT scaling to a collection of cones. fn scale_cones(&self, cones: &mut Self::C, μ: T, scaling_strategy: ScalingStrategy) -> bool; /// Compute the barrier function - fn barrier(&self, step: &Self, α: T, cones: &Self::C) -> T; + fn barrier(&self, step: &Self, α: T, cones: &mut Self::C) -> T; /// Rescale variables, e.g. to renormalize iterates /// in a homogeneous embedding diff --git a/src/solver/implementations/default/variables.rs b/src/solver/implementations/default/variables.rs index 55f9d1ea..117e731b 100644 --- a/src/solver/implementations/default/variables.rs +++ b/src/solver/implementations/default/variables.rs @@ -193,7 +193,7 @@ where cones.update_scaling(&self.s, &self.z, μ, scaling_strategy) } - fn barrier(&self, step: &Self, α: T, cones: &CompositeCone) -> T { + fn barrier(&self, step: &Self, α: T, cones: &mut CompositeCone) -> T { let central_coef = (cones.degree() + 1).as_T(); let cur_τ = self.τ + α * step.τ; From 5f7a0805e7c6cf0db093cb8f81cc398f858528fe Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Mon, 4 Sep 2023 20:38:09 +0100 Subject: [PATCH 18/52] add itertools --- Cargo.toml | 1 + src/algebra/vecmath.rs | 3 ++- src/solver/core/cones/genpowcone.rs | 25 ++++++++++-------------- src/solver/core/cones/nonnegativecone.rs | 5 +++-- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5cd85612..da8d152c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ enum_dispatch = "0.3.8" amd = "0.2.2" thiserror = "1.0" cfg-if = "1.0" +itertools = "0.11" # ------------------------------- # features diff --git a/src/algebra/vecmath.rs b/src/algebra/vecmath.rs index 57d4cf01..06425267 100644 --- a/src/algebra/vecmath.rs +++ b/src/algebra/vecmath.rs @@ -1,4 +1,5 @@ use super::{FloatT, ScalarMath, VectorMath}; +use itertools::izip; use std::iter::zip; impl VectorMath for [T] { @@ -88,7 +89,7 @@ impl VectorMath for [T] { assert_eq!(s.len(), ds.len()); let mut out = T::zero(); - for ((&s, &ds), (&z, &dz)) in zip(zip(s, ds), zip(z, dz)) { + for (&s, &ds, &z, &dz) in izip!(s, ds, z, dz) { let si = s + α * ds; let zi = z + α * dz; out += si * zi; diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs index 6a798c8f..36b28253 100644 --- a/src/solver/core/cones/genpowcone.rs +++ b/src/solver/core/cones/genpowcone.rs @@ -3,7 +3,7 @@ use crate::{ algebra::*, solver::{core::ScalingStrategy, CoreSettings}, }; -use std::iter::zip; +use itertools::izip; // ------------------------------------- // Generalized Power Cone @@ -113,12 +113,10 @@ where fn unit_initialization(&self, z: &mut [T], s: &mut [T]) { let α = &self.α; + let dim1 = self.dim1(); - s[..self.dim1()] - .iter_mut() - .enumerate() - .for_each(|(i, s)| *s = (T::one() + α[i]).sqrt()); - s[self.dim1()..].iter_mut().for_each(|x| *x = T::zero()); + s[..dim1].scalarop_from(|αi| T::sqrt(T::one() + αi), &α); + s[dim1..].set(T::zero()); z.copy_from(&s); } @@ -169,18 +167,15 @@ where // y1 .= K.d1 .* x1 - coef_q.*K.q // NB: d1 is a vector - let y1 = &mut y[..dim1]; - let x1 = &x[..dim1]; - - zip(zip(y1, x1), zip(&self.d1, &self.q)) - .for_each(|((y, &x), (&d1, &q))| *y += d1 * x - coef_q * q); + for (y, &x, &d1, &q) in izip!(&mut y[..dim1], &x[..dim1], &self.d1, &self.q) { + *y += d1 * x - coef_q * q; + } // y2 .= K.d2 .* x2 - coef_r.*K.r. // NB: d2 is a scalar - let y2 = &mut y[dim1..]; - let x2 = &x[dim1..]; - - zip(zip(y2, x2), &self.r).for_each(|((y, &x), &r)| *y += self.d2 * x - coef_r * r); + for (y, &x, &r) in izip!(&mut y[dim1..], &x[dim1..], &self.r) { + *y += self.d2 * x - coef_r * r; + } y.axpby(coef_p, &self.p, T::one()); y.scale(self.μ); diff --git a/src/solver/core/cones/nonnegativecone.rs b/src/solver/core/cones/nonnegativecone.rs index d8665a9c..f7557afb 100644 --- a/src/solver/core/cones/nonnegativecone.rs +++ b/src/solver/core/cones/nonnegativecone.rs @@ -3,6 +3,7 @@ use crate::{ algebra::*, solver::{core::ScalingStrategy, CoreSettings}, }; +use itertools::izip; use std::iter::zip; // ------------------------------------- @@ -75,7 +76,7 @@ where _μ: T, _scaling_strategy: ScalingStrategy, ) -> bool { - for ((λ, w), (s, z)) in zip(zip(&mut self.λ, &mut self.w), zip(s, z)) { + for ((λ, w, s, z)) in izip!(&mut self.λ, &mut self.w, s, z) { *λ = T::sqrt((*s) * (*z)); *w = T::sqrt((*s) / (*z)); } @@ -151,7 +152,7 @@ where assert_eq!(dz.len(), z.len()); assert_eq!(ds.len(), s.len()); let mut barrier = T::zero(); - for ((&s, &ds), (&z, &dz)) in zip(zip(s, ds), zip(z, dz)) { + for (&s, &ds, &z, &dz) in izip!(s, ds, z, dz) { let si = s + α * ds; let zi = z + α * dz; barrier += (si * zi).logsafe(); From 4bb1fa060c51b99b82b55c50a98b978425fc56d7 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Tue, 5 Sep 2023 10:56:55 +0100 Subject: [PATCH 19/52] Julia porting fixes --- src/algebra/csc/core.rs | 10 +- src/solver/core/cones/expcone.rs | 14 +- src/solver/core/cones/exppow_common.rs | 123 ++++++----- src/solver/core/cones/genpowcone.rs | 209 +++++++----------- src/solver/core/cones/nonnegativecone.rs | 2 +- src/solver/core/cones/powcone.rs | 44 +--- .../core/kktsolvers/direct/quasidef/utils.rs | 2 +- tests/basic_powcone.rs | 14 +- 8 files changed, 170 insertions(+), 248 deletions(-) diff --git a/src/algebra/csc/core.rs b/src/algebra/csc/core.rs index 4dc17b20..934dc06a 100644 --- a/src/algebra/csc/core.rs +++ b/src/algebra/csc/core.rs @@ -69,20 +69,18 @@ where J: IntoIterator, T: FloatT, { + #[allow(clippy::needless_range_loop)] fn from(rows: I) -> CscMatrix { let rows: Vec> = rows .into_iter() - .map(|r| r.into_iter().map(|&v| v).collect()) + .map(|r| r.into_iter().copied().collect()) .collect(); let m = rows.len(); let n = rows.iter().map(|r| r.len()).next().unwrap_or(0); + assert!(rows.iter().all(|r| r.len() == n)); - let nnz = rows - .iter() - .flat_map(|r| r) - .filter(|&&v| v != T::zero()) - .count(); + let nnz = rows.iter().flatten().filter(|&&v| v != T::zero()).count(); let mut colptr = Vec::with_capacity(n + 1); let mut rowval = Vec::with_capacity(nnz); diff --git a/src/solver/core/cones/expcone.rs b/src/solver/core/cones/expcone.rs index 473ad9c3..ab54459a 100644 --- a/src/solver/core/cones/expcone.rs +++ b/src/solver/core/cones/expcone.rs @@ -152,8 +152,8 @@ where let _is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; let _is_dual_feasible_fcn = |s: &[T]| -> bool { self.is_dual_feasible(s) }; - let αz = Self::backtrack_search(dz, z, αmax, αmin, step, _is_dual_feasible_fcn, &mut work); - let αs = Self::backtrack_search(ds, s, αmax, αmin, step, _is_prim_feasible_fcn, &mut work); + let αz = backtrack_search(dz, z, αmax, αmin, step, _is_dual_feasible_fcn, &mut work); + let αs = backtrack_search(ds, s, αmax, αmin, step, _is_prim_feasible_fcn, &mut work); (αz, αs) } @@ -236,7 +236,7 @@ where H[(2, 2)] = (r * r - z[0] * r + z[0] * z[0]) / (r * r * z[2] * z[2]); } - fn barrier_dual(&self, z: &[T]) -> T + fn barrier_dual(&mut self, z: &[T]) -> T where T: FloatT, { @@ -247,7 +247,7 @@ where -(-z[2] * z[0]).logsafe() - (z[1] - z[0] - z[0] * l).logsafe() } - fn barrier_primal(&self, s: &[T]) -> T + fn barrier_primal(&mut self, s: &[T]) -> T where T: FloatT, { @@ -284,7 +284,7 @@ where // Hψu = Hψ*u // gψ is used inside η - fn higher_correction(&mut self, η: &mut [T; 3], ds: &[T], v: &[T]) + fn higher_correction(&mut self, η: &mut [T], ds: &[T], v: &[T]) where T: FloatT, { @@ -312,8 +312,8 @@ where let ψ = z[0] * η[0] - z[0] + z[1]; - let dotψu = u.dot(&η[..]); - let dotψv = v.dot(&η[..]); + let dotψu = u.dot(η); + let dotψv = v.dot(η); let two: T = (2.).as_T(); let coef = diff --git a/src/solver/core/cones/exppow_common.rs b/src/solver/core/cones/exppow_common.rs index b38b5247..1e27be35 100644 --- a/src/solver/core/cones/exppow_common.rs +++ b/src/solver/core/cones/exppow_common.rs @@ -11,62 +11,13 @@ pub(crate) trait NonsymmetricCone { // Returns true if z is dual feasible fn is_dual_feasible(&self, z: &[T]) -> bool; - fn update_dual_grad_H(&mut self, z: &[T]); - - fn barrier_dual(&self, z: &[T]) -> T; + fn barrier_primal(&mut self, s: &[T]) -> T; - fn barrier_primal(&self, s: &[T]) -> T; + fn barrier_dual(&mut self, z: &[T]) -> T; - fn higher_correction(&mut self, η: &mut [T; 3], ds: &[T], v: &[T]); -} - -#[allow(clippy::too_many_arguments)] -pub(crate) trait NonsymmetricConeUtils { - fn backtrack_search( - dq: &[T], - q: &[T], - α_init: T, - α_min: T, - backtrack: T, - is_in_cone_fcn: impl Fn(&[T]) -> bool, - work: &mut [T], - ) -> T; -} + fn higher_correction(&mut self, η: &mut [T], ds: &[T], v: &[T]); -impl NonsymmetricConeUtils for C -where - T: FloatT, - C: NonsymmetricCone, -{ - // find the maximum step length α≥0 so that - // q + α*dq stays in an exponential or power - // cone, or their respective dual cones. - fn backtrack_search( - dq: &[T], - q: &[T], - α_init: T, - α_min: T, - backtrack: T, - is_in_cone_fcn: impl Fn(&[T]) -> bool, - work: &mut [T], - ) -> T { - let mut α = α_init; - - loop { - // work = q + α*dq - work.waxpby(T::one(), &q, α, &dq); - - if is_in_cone_fcn(work) { - break; - } - α *= backtrack; - if α < α_min { - α = T::zero(); - break; - } - } - α - } + fn update_dual_grad_H(&mut self, z: &[T]); } // -------------------------------------- @@ -201,5 +152,69 @@ where // return a 3D tuple for the primal gradient. pub(crate) trait NonsymmetricNDCone { // Compute the primal gradient of f(s) at s - fn minus_gradient_primal(&self, s: &[T]) -> (T, T); + fn gradient_primal(&self, grad: &mut [T], s: &[T]); +} + +// -------------------------------------- +// utility functions for nonsymmetric cones +// -------------------------------------- + +// find the maximum step length α≥0 so that +// q + α*dq stays in an exponential or power +// cone, or their respective dual cones. +pub(crate) fn backtrack_search( + dq: &[T], + q: &[T], + α_init: T, + α_min: T, + backtrack: T, + is_in_cone_fcn: impl Fn(&[T]) -> bool, + work: &mut [T], +) -> T +where + T: FloatT, +{ + let mut α = α_init; + + loop { + // work = q + α*dq + work.waxpby(T::one(), q, α, dq); + + if is_in_cone_fcn(work) { + break; + } + α *= backtrack; + if α < α_min { + α = T::zero(); + break; + } + } + α +} +pub(crate) fn newton_raphson_onesided(x0: T, f0: impl Fn(T) -> T, f1: impl Fn(T) -> T) -> T +where + T: FloatT, +{ + // implements NR method from a starting point assumed to be to the + // left of the true value. Once a negative step is encountered + // this function will halt regardless of the calculated correction. + + let mut x = x0; + let mut iter = 0; + + while iter < 100 { + iter += 1; + let dfdx = f1(x); + let dx = -f0(x) / dfdx; + + if (dx < T::epsilon()) + || (T::abs(dx / x) < T::sqrt(T::epsilon())) + || (T::abs(dfdx) < T::epsilon()) + { + break; + } + x += dx; + } + + x } diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs index 36b28253..442299cc 100644 --- a/src/solver/core/cones/genpowcone.rs +++ b/src/solver/core/cones/genpowcone.rs @@ -4,6 +4,7 @@ use crate::{ solver::{core::ScalingStrategy, CoreSettings}, }; use itertools::izip; +use std::iter::zip; // ------------------------------------- // Generalized Power Cone @@ -46,7 +47,7 @@ where let dim1 = α.len(); let dim = dim1 + dim2; - //PJG : these check belongs elsewhere + //PJG : these checks belong elsewhere assert!(α.iter().all(|r| *r > T::zero())); // check all powers are greater than 0 assert!((T::one() - α.sum()).abs() < T::epsilon()); @@ -115,10 +116,10 @@ where let α = &self.α; let dim1 = self.dim1(); - s[..dim1].scalarop_from(|αi| T::sqrt(T::one() + αi), &α); + s[..dim1].scalarop_from(|αi| T::sqrt(T::one() + αi), α); s[dim1..].set(T::zero()); - z.copy_from(&s); + z.copy_from(s); } fn set_identity_scaling(&mut self) { @@ -213,11 +214,11 @@ where //move "work" out of self let mut work = std::mem::take(&mut self.work); - let _is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; - let _is_dual_feasible_fcn = |s: &[T]| -> bool { self.is_dual_feasible(s) }; + let is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; + let is_dual_feasible_fcn = |s: &[T]| -> bool { self.is_dual_feasible(s) }; - let αz = Self::backtrack_search(dz, z, αmax, αmin, step, _is_dual_feasible_fcn, &mut work); - let αs = Self::backtrack_search(ds, s, αmax, αmin, step, _is_prim_feasible_fcn, &mut work); + let αz = backtrack_search(dz, z, αmax, αmin, step, is_dual_feasible_fcn, &mut work); + let αs = backtrack_search(ds, s, αmax, αmin, step, is_prim_feasible_fcn, &mut work); //restore work to self self.work = work; @@ -230,10 +231,10 @@ where let mut work = std::mem::take(&mut self.work); - work.waxpby(T::one(), &z, α, &dz); + work.waxpby(T::one(), z, α, dz); barrier += self.barrier_dual(&work); - work.waxpby(T::one(), &s, α, &ds); + work.waxpby(T::one(), s, α, ds); barrier += self.barrier_primal(&work); self.work = work; @@ -301,10 +302,8 @@ where let dim1 = self.dim1(); let two: T = (2.).as_T(); - let mut phi = T::one(); - for i in 0..dim1 { - phi *= (z[0] / α[i]).powf(two * α[i]) - } + let phi = zip(α, z).fold(T::one(), |phi, (&αi, &zi)| phi * (zi / αi).powf(two * αi)); + let norm2w = z[dim1..].sumsq(); let ζ = phi - norm2w; assert!(ζ > T::zero()); @@ -312,50 +311,35 @@ where // compute the gradient at z let grad = &mut self.grad; let τ = &mut self.q; - τ.iter_mut() - .enumerate() - .for_each(|(i, τ)| *τ = two * α[i] / z[i]); - grad[..dim1] - .iter_mut() - .enumerate() - .for_each(|(i, grad)| *grad = -τ[i] * phi / ζ - (T::one() - α[i]) / z[i]); - grad.iter_mut() - .enumerate() - .skip(dim1) - .for_each(|(i, x)| *x = two * z[i] / ζ); + + for (τ, grad, &α, &z) in izip!(τ.iter_mut(), &mut grad[..dim1], α, &z[..dim1]) { + *τ = two * α * (z / α).logsafe(); + *grad = -(*τ) * phi / ζ - (T::one() - α) / z; + } + + grad[dim1..].scalarop_from(|z| (two / ζ) * z, &z[dim1..]); // compute Hessian information at z - let p0 = (phi * (phi + norm2w) / two).sqrt(); + let p0 = T::sqrt(phi * (phi + norm2w) / two); let p1 = -two * phi / p0; - let q0 = (ζ * phi / two).sqrt(); - let r1 = two * (ζ / (phi + norm2w)).sqrt(); + let q0 = T::sqrt(ζ * phi / two); + let r1 = two * T::sqrt(ζ / (phi + norm2w)); // compute the diagonal d1,d2 - let d1 = &mut self.d1; - d1.iter_mut() - .enumerate() - .for_each(|(i, d1)| *d1 = τ[i] * phi / (ζ * z[i]) + (T::one() - α[i]) / (z[i] * z[i])); + for (d1, &τ, &α, &z) in izip!(&mut self.d1, τ.iter(), α, &z[..dim1]) { + *d1 = (τ) * phi / (ζ * z) + (T::one() - α) / (z * z); + } self.d2 = two / ζ; // compute p, q, r where τ shares memory with q - let c1 = p0 / ζ; - let p = &mut self.p; - p[..dim1].copy_from(&τ); - p[..dim1].iter_mut().for_each(|x| *x *= c1); - let c2 = p1 / ζ; - p[dim1..].copy_from(&z[dim1..]); - p[dim1..].iter_mut().for_each(|x| *x *= c2); - - let c3 = q0 / ζ; - let q = &mut self.q; - q.iter_mut().for_each(|x| *x *= c3); - let c4 = r1 / ζ; - let r = &mut self.r; - r.copy_from(&z[dim1..]); - r.iter_mut().for_each(|x| *x *= c4); + self.p[..dim1].scalarop_from(|τi| (p0 / ζ) * τi, τ); + self.p[dim1..].scalarop_from(|zi| (p1 / ζ) * zi, &z[dim1..]); + + self.q.scale(q0 / ζ); + self.r.scalarop_from(|zi| (r1 / ζ) * zi, &z[dim1..]); } - fn barrier_dual(&self, z: &[T]) -> T + fn barrier_dual(&mut self, z: &[T]) -> T where T: FloatT, { @@ -363,62 +347,41 @@ where let α = &self.α; let dim1 = self.dim1(); let two: T = (2.).as_T(); - let mut res = T::zero(); - for i in 0..dim1 { - res += two * α[i] * (z[i] / α[i]).logsafe(); + let mut res = T::zero(); + for (&zi, &αi) in zip(&z[..dim1], α) { + res += two * αi * (zi / αi).logsafe(); } res = T::exp(res) - z[dim1..].sumsq(); let mut barrier: T = -res.logsafe(); - z[..dim1] - .iter() - .enumerate() - .for_each(|(i, x)| barrier -= (*x).logsafe() * (T::one() - α[i])); + for (&zi, &αi) in zip(&z[..dim1], α) { + barrier -= (zi).logsafe() * (T::one() - αi); + } barrier } - fn barrier_primal(&self, s: &[T]) -> T + fn barrier_primal(&mut self, s: &[T]) -> T where T: FloatT, { // Primal barrier: f(s) = ⟨s,g(s)⟩ - f*(-g(s)) - // NB: ⟨s,g(s)⟩ = -(dim1+1) = - ν - let α = &self.α; + // NB: ⟨s,g(s)⟩ = -(dim1(K)+1) = - ν - //YC: Do we need to care about the memory allocation time for minus_q? - let (g1, norm_r) = self.minus_gradient_primal(s); - let mut minus_g = Vec::with_capacity(self.dim()); + let mut g = std::mem::take(&mut self.work); - let dim1 = self.dim1(); - if norm_r > T::epsilon() { - minus_g - .iter_mut() - .enumerate() - .skip(dim1) - .for_each(|(i, x)| *x = g1 * s[i] / norm_r); - minus_g[..dim1] - .iter_mut() - .enumerate() - .for_each(|(i, x)| *x = -(T::one() + α[i] + α[i] * g1 * norm_r) / s[i]); - } else { - minus_g.iter_mut().skip(dim1).for_each(|x| *x = T::zero()); - minus_g[..dim1] - .iter_mut() - .enumerate() - .for_each(|(i, x)| *x = -(T::one() + α[i]) / s[i]); - } + self.gradient_primal(&mut g, s); + g.negate(); //-g(s) - let minus_one = (-1.).as_T(); - minus_g.iter_mut().for_each(|x| *x *= minus_one); // add the sign to it, i.e. return -g + let out = -self.barrier_dual(&g) - self.degree().as_T(); - let out = -self.barrier_dual(&minus_g) - self.degree().as_T(); + self.work = g; out } - fn higher_correction(&mut self, _η: &mut [T; 3], _ds: &[T], _v: &[T]) { + fn higher_correction(&mut self, _η: &mut [T], _ds: &[T], _v: &[T]) { unimplemented!() } } @@ -428,31 +391,37 @@ where T: FloatT, { // Compute the primal gradient of f(s) at s - fn minus_gradient_primal(&self, s: &[T]) -> (T, T) + fn gradient_primal(&self, g: &mut [T], s: &[T]) where T: FloatT, { - let α = &self.α; let dim1 = self.dim1(); let two: T = (2.).as_T(); // unscaled phi - let mut phi = T::one(); - for i in 0..dim1 { - phi *= s[i].powf(two * α[i]); - } + let phi = + zip(&s[..dim1], &self.α).fold(T::one(), |phi, (&si, &αi)| phi * si.powf(two * αi)); // obtain g1 from the Newton-Raphson method - let norm_r = s[dim1..].norm(); - let mut g1 = T::zero(); + let (p, r) = s.split_at(dim1); + let (gp, gr) = g.split_at_mut(dim1); + let norm_r = r.norm(); if norm_r > T::epsilon() { - g1 = _newton_raphson_genpowcone(norm_r, &s[..dim1], phi, α, self.ψ); - // minus_g.iter_mut().enumerate().skip(dim1).for_each(|(i,x)| *x = g1*s[i]/norm_r); - // minus_g[..dim1].iter_mut().enumerate().for_each(|(i,x)| *x = -(T::one()+α[i]+α[i]*g1*norm_r)/s[i]); - } + let g1 = _newton_raphson_genpowcone(norm_r, p, phi, &self.α, self.ψ); + + gr.scalarop_from(|r| (g1 / norm_r) * r, &self.r); + + for (gp, &α, &p) in izip!(gp.iter_mut(), &self.α, p) { + *gp = -(T::one() + α + α * g1 * norm_r) / p; + } + } else { + gr.set(T::zero()); - (g1, norm_r) + for (gp, &α, &p) in izip!(gp.iter_mut(), &self.α, p) { + *gp = -(T::one() + α) / p; + } + } } } // ---------------------------------------------- @@ -464,7 +433,7 @@ where // When we initialize x0 such that 0 < x0 < x* and f(x0) > 0, // the Newton-Raphson method converges quadratically -fn _newton_raphson_genpowcone(norm_r: T, p: &[T], phi: T, α: &Vec, ψ: T) -> T +fn _newton_raphson_genpowcone(norm_r: T, p: &[T], phi: T, α: &[T], ψ: T) -> T where T: FloatT, { @@ -478,53 +447,23 @@ where // function for f(x) = 0 let f0 = { |x: T| -> T { - let mut fval = -(two * x / norm_r + x * x).logsafe(); - α.iter().enumerate().for_each(|(i, &αi)| { - fval += two * αi * ((x * norm_r + (T::one() + αi) / αi).logsafe() - p[i].logsafe()) - }); + let finit = -(two * x / norm_r + x * x).logsafe(); - fval + zip(α, p).fold(finit, |f, (&αi, &pi)| { + f + two * αi * ((x * norm_r + (T::one() + αi) / αi).logsafe() - pi.logsafe()) + }) } }; // first derivative let f1 = { |x: T| -> T { - let mut fval = -(two * x + two / norm_r) / (x * x + two * x / norm_r); - α.iter() - .for_each(|&αi| fval += two * (αi) * norm_r / (norm_r * x + (T::one() + αi) / αi)); + let finit = -(two * x + two / norm_r) / (x * x + two * x / norm_r); - fval + α.iter().fold(finit, |f, &αi| { + f + two * (αi) * norm_r / (norm_r * x + (T::one() + αi) / αi) + }) } }; - _newton_raphson_onesided(x0, f0, f1) -} - -// YC: it is the duplicate of the same one for power cones. Can we unify them into a common one? -fn _newton_raphson_onesided(x0: T, f0: impl Fn(T) -> T, f1: impl Fn(T) -> T) -> T -where - T: FloatT, -{ - // implements NR method from a starting point assumed to be to the - // left of the true value. Once a negative step is encountered - // this function will halt regardless of the calculated correction. - - let mut x = x0; - let mut iter = 0; - - while iter < 100 { - iter += 1; - let dfdx = f1(x); - let dx = -f0(x) / dfdx; - - if (dx < T::epsilon()) - || (T::abs(dx / x) < T::sqrt(T::epsilon())) - || (T::abs(dfdx) < T::epsilon()) - { - break; - } - x += dx; - } - - x + newton_raphson_onesided(x0, f0, f1) } diff --git a/src/solver/core/cones/nonnegativecone.rs b/src/solver/core/cones/nonnegativecone.rs index f7557afb..70e11c57 100644 --- a/src/solver/core/cones/nonnegativecone.rs +++ b/src/solver/core/cones/nonnegativecone.rs @@ -76,7 +76,7 @@ where _μ: T, _scaling_strategy: ScalingStrategy, ) -> bool { - for ((λ, w, s, z)) in izip!(&mut self.λ, &mut self.w, s, z) { + for (λ, w, s, z) in izip!(&mut self.λ, &mut self.w, s, z) { *λ = T::sqrt((*s) * (*z)); *w = T::sqrt((*s) / (*z)); } diff --git a/src/solver/core/cones/powcone.rs b/src/solver/core/cones/powcone.rs index a1d867de..d74ff7e5 100644 --- a/src/solver/core/cones/powcone.rs +++ b/src/solver/core/cones/powcone.rs @@ -155,8 +155,8 @@ where let _is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; let _is_dual_feasible_fcn = |s: &[T]| -> bool { self.is_dual_feasible(s) }; - let αz = Self::backtrack_search(dz, z, αmax, αmin, step, _is_dual_feasible_fcn, &mut work); - let αs = Self::backtrack_search(ds, s, αmax, αmin, step, _is_prim_feasible_fcn, &mut work); + let αz = backtrack_search(dz, z, αmax, αmin, step, _is_dual_feasible_fcn, &mut work); + let αs = backtrack_search(ds, s, αmax, αmin, step, _is_prim_feasible_fcn, &mut work); (αz, αs) } @@ -253,7 +253,7 @@ where grad[2] = two * z[2] / ψ; } - fn barrier_dual(&self, z: &[T]) -> T + fn barrier_dual(&mut self, z: &[T]) -> T where T: FloatT, { @@ -267,7 +267,7 @@ where -arg1.logsafe() - (T::one() - α) * z[0].logsafe() - α * z[1].logsafe() } - fn barrier_primal(&self, s: &[T]) -> T + fn barrier_primal(&mut self, s: &[T]) -> T where T: FloatT, { @@ -301,7 +301,7 @@ where // 4*α*(1-α)*ϕ/(z1*z2) 2*(1-α)*(1-2*α)*ϕ/(z2*z2) 0; // 0 0 -2;] - fn higher_correction(&mut self, η: &mut [T; 3], ds: &[T], v: &[T]) + fn higher_correction(&mut self, η: &mut [T], ds: &[T], v: &[T]) where T: FloatT, { @@ -346,8 +346,8 @@ where Hψ[(1, 2)] = T::zero(); Hψ[(2, 2)] = -two; - let dotψu = u.dot(&η[..]); - let dotψv = v.dot(&η[..]); + let dotψu = u.dot(η); + let dotψv = v.dot(η); let mut Hψv = [T::zero(); 3]; Hψ.mul(&mut Hψv, v); @@ -483,33 +483,5 @@ where - ((x + s3.recip()) * two) / (t1 + t2) } }; - _newton_raphson_onesided(x0, f0, f1) -} - -fn _newton_raphson_onesided(x0: T, f0: impl Fn(T) -> T, f1: impl Fn(T) -> T) -> T -where - T: FloatT, -{ - // implements NR method from a starting point assumed to be to the - // left of the true value. Once a negative step is encountered - // this function will halt regardless of the calculated correction. - - let mut x = x0; - let mut iter = 0; - - while iter < 100 { - iter += 1; - let dfdx = f1(x); - let dx = -f0(x) / dfdx; - - if (dx < T::epsilon()) - || (T::abs(dx / x) < T::sqrt(T::epsilon())) - || (T::abs(dfdx) < T::epsilon()) - { - break; - } - x += dx; - } - - x + newton_raphson_onesided(x0, f0, f1) } diff --git a/src/solver/core/kktsolvers/direct/quasidef/utils.rs b/src/solver/core/kktsolvers/direct/quasidef/utils.rs index 01a2e6cd..960f1212 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/utils.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/utils.rs @@ -177,7 +177,7 @@ fn _kkt_assemble_colcounts( K.colcount_rowvec(nvars, col + 2, headidx + n); // p row } } - genpowidx = genpowidx + 1; + genpowidx += 1; } } diff --git a/tests/basic_powcone.rs b/tests/basic_powcone.rs index 414c5507..f1818a7f 100644 --- a/tests/basic_powcone.rs +++ b/tests/basic_powcone.rs @@ -3,7 +3,7 @@ use clarabel::{algebra::*, solver::*}; #[test] -fn test_powcone_feasible() { +fn test_powcone() { // solve the following power cone problem // max x1^0.6 y^0.4 + x2^0.1 // s.t. x1, y, x2 >= 0 @@ -15,6 +15,7 @@ fn test_powcone_feasible() { // x1 + 2y + 3x2 == 3 // x = (x1, y, z1, x2, y2, z2) + let n = 6; let P = CscMatrix::::zeros((n, n)); let c = vec![0., 0., -1., 0., 0., -1.]; @@ -28,13 +29,10 @@ fn test_powcone_feasible() { // x1 + 2y + 3x2 == 3 // y2 == 1 - let A2 = CscMatrix::new( - 2, // m - 6, // n - vec![0, 1, 2, 2, 3, 4, 4], //colptr - vec![0, 0, 0, 1], //rowval - vec![1., 2., 3., 1.], //nzval - ); + let A2 = CscMatrix::from(&[ + [1., 2., 0., 3., 0., 0.], // + [0., 0., 0., 0., 1., 0.], // + ]); let b2 = vec![3., 1.]; let cones2 = vec![ZeroConeT(2)]; From ab31f32a094923c5e6d264db3c69423f1814bea3 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Tue, 5 Sep 2023 11:01:52 +0100 Subject: [PATCH 20/52] add genpow test --- tests/basic_genpowcone.rs | 55 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/basic_genpowcone.rs diff --git a/tests/basic_genpowcone.rs b/tests/basic_genpowcone.rs new file mode 100644 index 00000000..38921d26 --- /dev/null +++ b/tests/basic_genpowcone.rs @@ -0,0 +1,55 @@ +#![allow(non_snake_case)] + +use clarabel::{algebra::*, solver::*}; + +#[test] +fn test_powcone() { + // solve the following power cone problem + // max x1^0.6 y^0.4 + x2^0.1 + // s.t. x1, y, x2 >= 0 + // x1 + 2y + 3x2 == 3 + // which is equivalent to + // max z1 + z2 + // s.t. (x1, y, z1) in K_pow(0.6) + // (x2, 1, z2) in K_pow(0.1) + // x1 + 2y + 3x2 == 3 + + // x = (x1, y, z1, x2, y2, z2) + let n = 6; + let P = CscMatrix::::zeros((n, n)); + let c = vec![0., 0., -1., 0., 0., -1.]; + + // (x1, y, z1) in K_pow(0.6) + // (x2, y2, z2) in K_pow(0.1) + let mut A1 = CscMatrix::::identity(n); + A1.negate(); + let b1 = vec![0.; n]; + let cones1 = vec![ + GenPowerConeT(vec![0.6, 0.4], 1), + GenPowerConeT(vec![0.1, 0.9], 1), + ]; + + // x1 + 2y + 3x2 == 3 + // y2 == 1 + let A2 = CscMatrix::from(&[ + [1., 2., 0., 3., 0., 0.], // + [0., 0., 0., 0., 1., 0.], // + ]); + + let b2 = vec![3., 1.]; + let cones2 = vec![ZeroConeT(2)]; + + let A = CscMatrix::vcat(&A1, &A2); + let b = [b1, b2].concat(); + let cones = [cones1, cones2].concat(); + + let settings = DefaultSettings::default(); + let mut solver = DefaultSolver::new(&P, &c, &A, &b, &cones, settings); + + solver.solve(); + + assert_eq!(solver.solution.status, SolverStatus::Solved); + + let refobj = -1.8458; + assert!(f64::abs(solver.info.cost_primal - refobj) <= 1e-3); +} From 45452901be51101f7ecc1226a81e1b315c26b3f5 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Tue, 5 Sep 2023 14:14:22 +0100 Subject: [PATCH 21/52] update NR method --- src/solver/core/cones/powcone.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/solver/core/cones/powcone.rs b/src/solver/core/cones/powcone.rs index d74ff7e5..fd7e93cc 100644 --- a/src/solver/core/cones/powcone.rs +++ b/src/solver/core/cones/powcone.rs @@ -451,8 +451,13 @@ where // init point x0: since our dual barrier has an additional // shift -2α*log(α) - 2(1-α)*log(1-α) > 0 in f(x), // the previous selection is still feasible, i.e. f(x0) > 0 + + // PJG: Not clear which one of these is correct. Second + // one is from Julia yc/genpow branch. First is from main let x0 = -s3.recip() + (s3 + ((phi * four) * phi / s3 / s3 + three * phi).sqrt()) * two / (phi * four - s3 * s3); + let x0 = + -s3.recip() + (s3 * two + T::sqrt((phi * phi) / (s3 * s3) + phi * three)) / (phi - s3 * s3); // additional shift due to the choice of dual barrier let t0 = -two * α * (α.logsafe()) - two * (T::one() - α) * (T::one() - α).logsafe(); From d0f2948c0949bf9c43b0fabbe4a1720a006cc497 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Tue, 5 Sep 2023 15:31:10 +0100 Subject: [PATCH 22/52] bug fix --- src/solver/core/cones/genpowcone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs index 442299cc..bd59f0a0 100644 --- a/src/solver/core/cones/genpowcone.rs +++ b/src/solver/core/cones/genpowcone.rs @@ -313,7 +313,7 @@ where let τ = &mut self.q; for (τ, grad, &α, &z) in izip!(τ.iter_mut(), &mut grad[..dim1], α, &z[..dim1]) { - *τ = two * α * (z / α).logsafe(); + *τ = two * α / z; *grad = -(*τ) * phi / ζ - (T::one() - α) / z; } From 76892d8cba8982104708db8792162138ea4e2b40 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Tue, 5 Sep 2023 16:37:12 +0100 Subject: [PATCH 23/52] adds cone method. adds extra work field to GPC --- src/solver/core/cones/compositecone.rs | 7 +++++++ src/solver/core/cones/expcone.rs | 4 ++++ src/solver/core/cones/genpowcone.rs | 13 +++++++++++-- src/solver/core/cones/mod.rs | 4 ++++ src/solver/core/cones/nonnegativecone.rs | 4 ++++ src/solver/core/cones/powcone.rs | 9 ++++----- src/solver/core/cones/socone.rs | 4 ++++ src/solver/core/cones/zerocone.rs | 4 ++++ src/solver/core/solver.rs | 5 ++++- 9 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/solver/core/cones/compositecone.rs b/src/solver/core/cones/compositecone.rs index e793504e..e4212b54 100644 --- a/src/solver/core/cones/compositecone.rs +++ b/src/solver/core/cones/compositecone.rs @@ -182,6 +182,12 @@ where self._is_symmetric } + fn allows_primal_dual_scaling(&self) -> bool { + self.cones + .iter() + .all(|cone| cone.allows_primal_dual_scaling()) + } + fn rectify_equilibration(&self, δ: &mut [T], e: &[T]) -> bool { let mut any_changed = false; @@ -245,6 +251,7 @@ where } fn Hs_is_diagonal(&self) -> bool { + //PJG: replace with "all" here //This function should probably never be called since //we only us it to interrogate the blocks, but we can //implement something reasonable anyway diff --git a/src/solver/core/cones/expcone.rs b/src/solver/core/cones/expcone.rs index ab54459a..d986f5f2 100644 --- a/src/solver/core/cones/expcone.rs +++ b/src/solver/core/cones/expcone.rs @@ -53,6 +53,10 @@ where false } + fn allows_primal_dual_scaling(&self) -> bool { + true + } + fn rectify_equilibration(&self, δ: &mut [T], e: &[T]) -> bool { δ.copy_from(e).recip().scale(e.mean()); true // scalar equilibration diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs index bd59f0a0..b450480c 100644 --- a/src/solver/core/cones/genpowcone.rs +++ b/src/solver/core/cones/genpowcone.rs @@ -37,6 +37,8 @@ pub struct GenPowerCone { //work vector length dim, e.g. for line searches work: Vec, + //work vector exclusively for computing the primal barrier function. + work_pb: Vec, } impl GenPowerCone @@ -66,6 +68,7 @@ where d2: T::zero(), ψ, work: vec![T::zero(); dim], + work_pb: vec![T::zero(); dim], } } @@ -96,6 +99,10 @@ where false } + fn allows_primal_dual_scaling(&self) -> bool { + false + } + fn rectify_equilibration(&self, δ: &mut [T], e: &[T]) -> bool { δ.copy_from(e).recip().scale(e.mean()); true // scalar equilibration @@ -369,14 +376,16 @@ where // Primal barrier: f(s) = ⟨s,g(s)⟩ - f*(-g(s)) // NB: ⟨s,g(s)⟩ = -(dim1(K)+1) = - ν - let mut g = std::mem::take(&mut self.work); + // can't use "work" here because it was already + // used to construct the argument s in some cases + let mut g = std::mem::take(&mut self.work_pb); self.gradient_primal(&mut g, s); g.negate(); //-g(s) let out = -self.barrier_dual(&g) - self.degree().as_T(); - self.work = g; + self.work_pb = g; out } diff --git a/src/solver/core/cones/mod.rs b/src/solver/core/cones/mod.rs index 311cc77f..6374881a 100644 --- a/src/solver/core/cones/mod.rs +++ b/src/solver/core/cones/mod.rs @@ -48,8 +48,12 @@ where fn degree(&self) -> usize; fn numel(&self) -> usize; + // is the cone symmetric? NB: zero cone still reports true fn is_symmetric(&self) -> bool; + // report false here if only dual scaling is implemented (e.g. GenPowerCone) + fn allows_primal_dual_scaling(&self) -> bool; + // converts an elementwise scaling into // a scaling that preserves cone memership fn rectify_equilibration(&self, δ: &mut [T], e: &[T]) -> bool; diff --git a/src/solver/core/cones/nonnegativecone.rs b/src/solver/core/cones/nonnegativecone.rs index 70e11c57..b7e9d7f1 100644 --- a/src/solver/core/cones/nonnegativecone.rs +++ b/src/solver/core/cones/nonnegativecone.rs @@ -45,6 +45,10 @@ where true } + fn allows_primal_dual_scaling(&self) -> bool { + true + } + fn rectify_equilibration(&self, δ: &mut [T], _e: &[T]) -> bool { δ.set(T::one()); false diff --git a/src/solver/core/cones/powcone.rs b/src/solver/core/cones/powcone.rs index fd7e93cc..d4025c19 100644 --- a/src/solver/core/cones/powcone.rs +++ b/src/solver/core/cones/powcone.rs @@ -55,6 +55,10 @@ where false } + fn allows_primal_dual_scaling(&self) -> bool { + true + } + fn rectify_equilibration(&self, δ: &mut [T], e: &[T]) -> bool { δ.copy_from(e).recip().scale(e.mean()); true // scalar equilibration @@ -446,16 +450,11 @@ where { let two: T = (2.).as_T(); let three: T = (3.).as_T(); - let four: T = (4.).as_T(); // init point x0: since our dual barrier has an additional // shift -2α*log(α) - 2(1-α)*log(1-α) > 0 in f(x), // the previous selection is still feasible, i.e. f(x0) > 0 - // PJG: Not clear which one of these is correct. Second - // one is from Julia yc/genpow branch. First is from main - let x0 = -s3.recip() - + (s3 + ((phi * four) * phi / s3 / s3 + three * phi).sqrt()) * two / (phi * four - s3 * s3); let x0 = -s3.recip() + (s3 * two + T::sqrt((phi * phi) / (s3 * s3) + phi * three)) / (phi - s3 * s3); diff --git a/src/solver/core/cones/socone.rs b/src/solver/core/cones/socone.rs index 4999d9de..2f7e28ba 100644 --- a/src/solver/core/cones/socone.rs +++ b/src/solver/core/cones/socone.rs @@ -57,6 +57,10 @@ where true } + fn allows_primal_dual_scaling(&self) -> bool { + true + } + fn rectify_equilibration(&self, δ: &mut [T], e: &[T]) -> bool { δ.copy_from(e).recip().scale(e.mean()); diff --git a/src/solver/core/cones/zerocone.rs b/src/solver/core/cones/zerocone.rs index 281c16f7..b83fe006 100644 --- a/src/solver/core/cones/zerocone.rs +++ b/src/solver/core/cones/zerocone.rs @@ -42,6 +42,10 @@ where true } + fn allows_primal_dual_scaling(&self) -> bool { + true + } + fn rectify_equilibration(&self, δ: &mut [T], _e: &[T]) -> bool { δ.set(T::one()); false diff --git a/src/solver/core/solver.rs b/src/solver/core/solver.rs index 5486e0df..3056ff7d 100644 --- a/src/solver/core/solver.rs +++ b/src/solver/core/solver.rs @@ -196,7 +196,10 @@ where // main loop // ---------- - let mut scaling = ScalingStrategy::PrimalDual; + let mut scaling ={ + if self.cones.allows_primal_dual_scaling() {ScalingStrategy::PrimalDual} + else {ScalingStrategy::Dual} + }; loop { From 738c526cc189de97ae2bdfef9153098e1b56c16c Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Tue, 5 Sep 2023 16:51:13 +0100 Subject: [PATCH 24/52] spacing --- src/solver/core/solver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solver/core/solver.rs b/src/solver/core/solver.rs index 3056ff7d..236b1d63 100644 --- a/src/solver/core/solver.rs +++ b/src/solver/core/solver.rs @@ -196,7 +196,7 @@ where // main loop // ---------- - let mut scaling ={ + let mut scaling = { if self.cones.allows_primal_dual_scaling() {ScalingStrategy::PrimalDual} else {ScalingStrategy::Dual} }; From 6336a652aa95b6fbda0efdb86a3bcb60962732a8 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Tue, 5 Sep 2023 17:34:27 +0100 Subject: [PATCH 25/52] simplify implementation of Hs_is_diagonal --- src/solver/core/cones/compositecone.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/solver/core/cones/compositecone.rs b/src/solver/core/cones/compositecone.rs index e4212b54..475d47bd 100644 --- a/src/solver/core/cones/compositecone.rs +++ b/src/solver/core/cones/compositecone.rs @@ -251,18 +251,10 @@ where } fn Hs_is_diagonal(&self) -> bool { - //PJG: replace with "all" here //This function should probably never be called since //we only us it to interrogate the blocks, but we can //implement something reasonable anyway - let mut is_diag = true; - for cone in self.iter() { - is_diag &= cone.Hs_is_diagonal(); - if !is_diag { - break; - } - } - is_diag + self.cones.iter().all(|cone| cone.Hs_is_diagonal()) } #[allow(non_snake_case)] From 6355cb1fe9b73c78889260bf172115b7d8f70ab0 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Tue, 5 Sep 2023 17:34:45 +0100 Subject: [PATCH 26/52] sync Julia nonsym cones --- src/solver/core/cones/genpowcone.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs index b450480c..c027afe7 100644 --- a/src/solver/core/cones/genpowcone.rs +++ b/src/solver/core/cones/genpowcone.rs @@ -176,13 +176,13 @@ where // y1 .= K.d1 .* x1 - coef_q.*K.q // NB: d1 is a vector for (y, &x, &d1, &q) in izip!(&mut y[..dim1], &x[..dim1], &self.d1, &self.q) { - *y += d1 * x - coef_q * q; + *y = d1 * x - coef_q * q; } // y2 .= K.d2 .* x2 - coef_r.*K.r. // NB: d2 is a scalar for (y, &x, &r) in izip!(&mut y[dim1..], &x[dim1..], &self.r) { - *y += self.d2 * x - coef_r * r; + *y = self.d2 * x - coef_r * r; } y.axpby(coef_p, &self.p, T::one()); From e13436d560a33fc5e624e79ea304f157771fe178 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Tue, 5 Sep 2023 18:23:00 +0100 Subject: [PATCH 27/52] reduce wright omega iterations --- src/solver/core/cones/expcone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solver/core/cones/expcone.rs b/src/solver/core/cones/expcone.rs index d986f5f2..d384eb7c 100644 --- a/src/solver/core/cones/expcone.rs +++ b/src/solver/core/cones/expcone.rs @@ -434,7 +434,7 @@ where let mut r = z - w - w.logsafe(); // Santiago suggests two refinement iterations only - for _ in 0..3 { + for _ in 0..2 { let wp1 = w + T::one(); let t = wp1 * (wp1 + (r * (2.).as_T()) / (3.0).as_T()); w *= T::one() + (r / wp1) * (t - r * (0.5).as_T()) / (t - r); From 5a167d5a4d1e6c3f5f2b0045b07079ce6bfb8392 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Tue, 5 Sep 2023 23:00:02 +0100 Subject: [PATCH 28/52] clippy appeasement --- src/algebra/floats.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/algebra/floats.rs b/src/algebra/floats.rs index 5156f489..26bed82b 100644 --- a/src/algebra/floats.rs +++ b/src/algebra/floats.rs @@ -1,3 +1,4 @@ +#![allow(non_snake_case)] use num_traits::{Float, FloatConst, FromPrimitive, NumAssign}; use std::fmt::{Debug, Display, LowerExp}; @@ -79,8 +80,6 @@ cfg_if::cfg_if! { // NB: `AsFloatT` is a convenience trait for f32/64 and u32/64 // so that we can do things like (2.0).as_T() everywhere on // constants, rather than the awful T::from_f32(2.0).unwrap() - -#[allow(non_snake_case)] pub trait AsFloatT: 'static { fn as_T(&self) -> T; } From 5877804ff94bb2f17927caa919cd8ea060b95807 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Thu, 7 Sep 2023 23:24:16 +0100 Subject: [PATCH 29/52] rewrite of KKT assembly and cone data mappings --- src/solver/core/cones/compositecone.rs | 2 +- src/solver/core/cones/expcone.rs | 112 ++--- src/solver/core/cones/genpowcone.rs | 181 ++++---- src/solver/core/cones/mod.rs | 4 +- src/solver/core/cones/nonnegativecone.rs | 2 +- ...xppow_common.rs => nonsymmetric_common.rs} | 0 src/solver/core/cones/powcone.rs | 118 ++--- src/solver/core/cones/psdtrianglecone.rs | 90 ++-- src/solver/core/cones/socone.rs | 2 +- src/solver/core/cones/supportedcone.rs | 23 +- src/solver/core/cones/zerocone.rs | 2 +- .../kktsolvers/direct/quasidef/datamap.rs | 95 ---- .../kktsolvers/direct/quasidef/datamaps.rs | 403 +++++++++++++++++ .../direct/quasidef/directldlkktsolver.rs | 132 ++---- .../direct/quasidef/kkt_assembly.rs | 264 ++++++++++++ .../core/kktsolvers/direct/quasidef/mod.rs | 8 +- .../core/kktsolvers/direct/quasidef/utils.rs | 406 ------------------ .../implementations/default/info_print.rs | 5 +- .../implementations/default/kktsystem.rs | 6 +- tests/basic_sdp.rs | 2 +- 20 files changed, 987 insertions(+), 870 deletions(-) rename src/solver/core/cones/{exppow_common.rs => nonsymmetric_common.rs} (100%) delete mode 100644 src/solver/core/kktsolvers/direct/quasidef/datamap.rs create mode 100644 src/solver/core/kktsolvers/direct/quasidef/datamaps.rs create mode 100644 src/solver/core/kktsolvers/direct/quasidef/kkt_assembly.rs delete mode 100644 src/solver/core/kktsolvers/direct/quasidef/utils.rs diff --git a/src/solver/core/cones/compositecone.rs b/src/solver/core/cones/compositecone.rs index 475d47bd..ab5e4e3a 100644 --- a/src/solver/core/cones/compositecone.rs +++ b/src/solver/core/cones/compositecone.rs @@ -157,7 +157,7 @@ where pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, SupportedCone> { self.cones.iter_mut() } - pub(crate) fn type_count(&self, tag: SupportedConeTag) -> usize { + pub(crate) fn get_type_count(&self, tag: SupportedConeTag) -> usize { if self.type_counts.contains_key(&tag) { self.type_counts[&tag] } else { diff --git a/src/solver/core/cones/expcone.rs b/src/solver/core/cones/expcone.rs index d384eb7c..619e88e4 100644 --- a/src/solver/core/cones/expcone.rs +++ b/src/solver/core/cones/expcone.rs @@ -8,7 +8,7 @@ use crate::{ // Exponential Cone // ------------------------------------- -pub struct ExponentialCone { +pub struct ExponentialCone { // Hessian of the dual barrier at z H_dual: DenseMatrixSym3, @@ -215,42 +215,6 @@ where false } - fn update_dual_grad_H(&mut self, z: &[T]) { - let grad = &mut self.grad; - let H = &mut self.H_dual; - - // Hessian computation, compute μ locally - let l = (-z[2] / z[0]).logsafe(); - let r = -z[0] * l - z[0] + z[1]; - - // compute the gradient at z - let c2 = r.recip(); - - grad[0] = c2 * l - z[0].recip(); - grad[1] = -c2; - grad[2] = (c2 * z[0] - T::one()) / z[2]; - - // compute_Hessian(K,z,H). Type is symmetric, so - // only need to assign upper triangle. - H[(0, 0)] = (r * r - z[0] * r + l * l * z[0] * z[0]) / (r * z[0] * z[0] * r); - H[(0, 1)] = -l / (r * r); - H[(1, 1)] = (r * r).recip(); - H[(0, 2)] = (z[1] - z[0]) / (r * r * z[2]); - H[(1, 2)] = -z[0] / (r * r * z[2]); - H[(2, 2)] = (r * r - z[0] * r + z[0] * z[0]) / (r * r * z[2] * z[2]); - } - - fn barrier_dual(&mut self, z: &[T]) -> T - where - T: FloatT, - { - // Dual barrier: - // f*(z) = -log(z2 - z1 - z1*log(z3/-z1)) - log(-z1) - log(z3) - // ----------------------------------------- - let l = (-z[2] / z[0]).logsafe(); - -(-z[2] * z[0]).logsafe() - (z[1] - z[0] - z[0] * l).logsafe() - } - fn barrier_primal(&mut self, s: &[T]) -> T where T: FloatT, @@ -268,25 +232,16 @@ where -ω.logsafe() - (s[1].logsafe()) * ((2.).as_T()) - s[2].logsafe() - (3.).as_T() } - // 3rd-order correction at the point z. Output is η. - // - // η = -0.5*[(dot(u,Hψ,v)*ψ - 2*dotψu*dotψv)/(ψ*ψ*ψ)*gψ + - // dotψu/(ψ*ψ)*Hψv + dotψv/(ψ*ψ)*Hψu - dotψuv/ψ + dothuv] - // - // where : - // Hψ = [ 1/z[1] 0 -1/z[3]; - // 0 0 0; - // -1/z[3] 0 z[1]/(z[3]*z[3]);] - // dotψuv = [-u[1]*v[1]/(z[1]*z[1]) + u[3]*v[3]/(z[3]*z[3]); - // 0; - // (u[3]*v[1]+u[1]*v[3])/(z[3]*z[3]) - 2*z[1]*u[3]*v[3]/(z[3]*z[3]*z[3])] - // - // dothuv = [-2*u[1]*v[1]/(z[1]*z[1]*z[1]) ; - // 0; - // -2*u[3]*v[3]/(z[3]*z[3]*z[3])] - // Hψv = Hψ*v - // Hψu = Hψ*u - // gψ is used inside η + fn barrier_dual(&mut self, z: &[T]) -> T + where + T: FloatT, + { + // Dual barrier: + // f*(z) = -log(z2 - z1 - z1*log(z3/-z1)) - log(-z1) - log(z3) + // ----------------------------------------- + let l = (-z[2] / z[0]).logsafe(); + -(-z[2] * z[0]).logsafe() - (z[1] - z[0] - z[0] * l).logsafe() + } fn higher_correction(&mut self, η: &mut [T], ds: &[T], v: &[T]) where @@ -341,6 +296,51 @@ where η[..].scale((0.5).as_T()); } + + // 3rd-order correction at the point z. Output is η. + // + // η = -0.5*[(dot(u,Hψ,v)*ψ - 2*dotψu*dotψv)/(ψ*ψ*ψ)*gψ + + // dotψu/(ψ*ψ)*Hψv + dotψv/(ψ*ψ)*Hψu - dotψuv/ψ + dothuv] + // + // where : + // Hψ = [ 1/z[1] 0 -1/z[3]; + // 0 0 0; + // -1/z[3] 0 z[1]/(z[3]*z[3]);] + // dotψuv = [-u[1]*v[1]/(z[1]*z[1]) + u[3]*v[3]/(z[3]*z[3]); + // 0; + // (u[3]*v[1]+u[1]*v[3])/(z[3]*z[3]) - 2*z[1]*u[3]*v[3]/(z[3]*z[3]*z[3])] + // + // dothuv = [-2*u[1]*v[1]/(z[1]*z[1]*z[1]) ; + // 0; + // -2*u[3]*v[3]/(z[3]*z[3]*z[3])] + // Hψv = Hψ*v + // Hψu = Hψ*u + // gψ is used inside η + + fn update_dual_grad_H(&mut self, z: &[T]) { + let grad = &mut self.grad; + let H = &mut self.H_dual; + + // Hessian computation, compute μ locally + let l = (-z[2] / z[0]).logsafe(); + let r = -z[0] * l - z[0] + z[1]; + + // compute the gradient at z + let c2 = r.recip(); + + grad[0] = c2 * l - z[0].recip(); + grad[1] = -c2; + grad[2] = (c2 * z[0] - T::one()) / z[2]; + + // compute_Hessian(K,z,H). Type is symmetric, so + // only need to assign upper triangle. + H[(0, 0)] = (r * r - z[0] * r + l * l * z[0] * z[0]) / (r * z[0] * z[0] * r); + H[(0, 1)] = -l / (r * r); + H[(1, 1)] = (r * r).recip(); + H[(0, 2)] = (z[1] - z[0]) / (r * r * z[2]); + H[(1, 2)] = -z[0] / (r * r * z[2]); + H[(2, 2)] = (r * r - z[0] * r + z[0] * z[0]) / (r * r * z[2] * z[2]); + } } impl Nonsymmetric3DCone for ExponentialCone diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs index c027afe7..0528371d 100644 --- a/src/solver/core/cones/genpowcone.rs +++ b/src/solver/core/cones/genpowcone.rs @@ -10,12 +10,7 @@ use std::iter::zip; // Generalized Power Cone // ------------------------------------- -pub struct GenPowerCone { - // power defining the cone. length determines dim1 - α: Vec, - // dimension of w - dim2: usize, - +pub struct GenPowerConeData { // gradient of the dual barrier at z grad: Vec, // holds copy of z at scaling point @@ -41,36 +36,48 @@ pub struct GenPowerCone { work_pb: Vec, } -impl GenPowerCone +impl GenPowerConeData where T: FloatT, { - pub fn new(α: Vec, dim2: usize) -> Self { + pub fn new(α: &Vec, dim2: usize) -> Self { let dim1 = α.len(); let dim = dim1 + dim2; //PJG : these checks belong elsewhere assert!(α.iter().all(|r| *r > T::zero())); // check all powers are greater than 0 - assert!((T::one() - α.sum()).abs() < T::epsilon()); - - let ψ = T::one() / (α.sumsq()); + assert!((T::one() - α.sum()).abs() < (T::epsilon() * α.len().as_T() * (0.5).as_T())); Self { - α, grad: vec![T::zero(); dim], z: vec![T::zero(); dim], - dim2, μ: T::one(), p: vec![T::zero(); dim], q: vec![T::zero(); dim1], r: vec![T::zero(); dim2], d1: vec![T::zero(); dim1], d2: T::zero(), - ψ, + ψ: T::one() / (α.sumsq()), work: vec![T::zero(); dim], work_pb: vec![T::zero(); dim], } } +} + +pub struct GenPowerCone { + pub α: Vec, // power defining the cone. length determines dim1 + pub dim2: usize, // dimension of w + pub data: Box>, // Boxed so that the enum_dispatch variant isn't huge +} + +impl GenPowerCone +where + T: FloatT, +{ + pub fn new(α: Vec, dim2: usize) -> Self { + let data = Box::new(GenPowerConeData::::new(&α, dim2)); + Self { α, dim2, data } + } pub fn dim1(&self) -> usize { self.α.len() @@ -144,10 +151,10 @@ where ) -> bool { // update both gradient and Hessian for function f*(z) at the point z self.update_dual_grad_H(z); - self.μ = μ; + self.data.μ = μ; // K.z .= z - self.z.copy_from(z); + self.data.z.copy_from(z); true } @@ -159,34 +166,36 @@ where fn get_Hs(&self, Hsblock: &mut [T]) { // we are returning here the diagonal D = [d1; d2] block let dim1 = self.dim1(); + let data = &self.data; - Hsblock[..dim1].scalarop_from(|x| self.μ * x, &self.d1); - Hsblock[dim1..].set(self.μ * self.d2); + Hsblock[..dim1].scalarop_from(|d1| data.μ * d1, &data.d1); + Hsblock[dim1..].set(data.μ * data.d2); } fn mul_Hs(&mut self, y: &mut [T], x: &[T], _work: &mut [T]) { // Hs = μ*(D + pp' -qq' -rr') let dim1 = self.dim1(); + let data = &self.data; - let coef_p = self.p.dot(x); - let coef_q = self.q.dot(&x[..dim1]); - let coef_r = self.r.dot(&x[dim1..]); + let coef_p = data.p.dot(x); + let coef_q = data.q.dot(&x[..dim1]); + let coef_r = data.r.dot(&x[dim1..]); // y1 .= K.d1 .* x1 - coef_q.*K.q // NB: d1 is a vector - for (y, &x, &d1, &q) in izip!(&mut y[..dim1], &x[..dim1], &self.d1, &self.q) { + for (y, &x, &d1, &q) in izip!(&mut y[..dim1], &x[..dim1], &data.d1, &data.q) { *y = d1 * x - coef_q * q; } // y2 .= K.d2 .* x2 - coef_r.*K.r. // NB: d2 is a scalar - for (y, &x, &r) in izip!(&mut y[dim1..], &x[dim1..], &self.r) { - *y = self.d2 * x - coef_r * r; + for (y, &x, &r) in izip!(&mut y[dim1..], &x[dim1..], &data.r) { + *y = data.d2 * x - coef_r * r; } - y.axpby(coef_p, &self.p, T::one()); - y.scale(self.μ); + y.axpby(coef_p, &data.p, T::one()); + y.scale(data.μ); } fn affine_ds(&self, ds: &mut [T], s: &[T]) { @@ -197,7 +206,7 @@ where &mut self, shift: &mut [T], _step_z: &mut [T], _step_s: &mut [T], σμ: T ) { //YC: No 3rd order correction at present - shift.scalarop_from(|g| g * σμ, &self.grad); + shift.scalarop_from(|g| g * σμ, &self.data.grad); } fn Δs_from_Δz_offset(&mut self, out: &mut [T], ds: &[T], _work: &mut [T], _z: &[T]) { @@ -219,7 +228,7 @@ where //simultaneously using "work" and the closures defined //below produces a borrow check error, so temporarily //move "work" out of self - let mut work = std::mem::take(&mut self.work); + let mut work = std::mem::take(&mut self.data.work); let is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; let is_dual_feasible_fcn = |s: &[T]| -> bool { self.is_dual_feasible(s) }; @@ -228,7 +237,7 @@ where let αs = backtrack_search(ds, s, αmax, αmin, step, is_prim_feasible_fcn, &mut work); //restore work to self - self.work = work; + self.data.work = work; (αz, αs) } @@ -236,7 +245,7 @@ where fn compute_barrier(&mut self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { let mut barrier = T::zero(); - let mut work = std::mem::take(&mut self.work); + let mut work = std::mem::take(&mut self.data.work); work.waxpby(T::one(), z, α, dz); barrier += self.barrier_dual(&work); @@ -244,7 +253,7 @@ where work.waxpby(T::one(), s, α, ds); barrier += self.barrier_primal(&work); - self.work = work; + self.data.work = work; barrier } @@ -304,46 +313,25 @@ where false } - fn update_dual_grad_H(&mut self, z: &[T]) { - let α = &self.α; - let dim1 = self.dim1(); - let two: T = (2.).as_T(); - - let phi = zip(α, z).fold(T::one(), |phi, (&αi, &zi)| phi * (zi / αi).powf(two * αi)); - - let norm2w = z[dim1..].sumsq(); - let ζ = phi - norm2w; - assert!(ζ > T::zero()); - - // compute the gradient at z - let grad = &mut self.grad; - let τ = &mut self.q; - - for (τ, grad, &α, &z) in izip!(τ.iter_mut(), &mut grad[..dim1], α, &z[..dim1]) { - *τ = two * α / z; - *grad = -(*τ) * phi / ζ - (T::one() - α) / z; - } + fn barrier_primal(&mut self, s: &[T]) -> T + where + T: FloatT, + { + // Primal barrier: f(s) = ⟨s,g(s)⟩ - f*(-g(s)) + // NB: ⟨s,g(s)⟩ = -(dim1(K)+1) = - ν - grad[dim1..].scalarop_from(|z| (two / ζ) * z, &z[dim1..]); + // can't use "work" here because it was already + // used to construct the argument s in some cases + let mut g = std::mem::take(&mut self.data.work_pb); - // compute Hessian information at z - let p0 = T::sqrt(phi * (phi + norm2w) / two); - let p1 = -two * phi / p0; - let q0 = T::sqrt(ζ * phi / two); - let r1 = two * T::sqrt(ζ / (phi + norm2w)); + self.gradient_primal(&mut g, s); + g.negate(); //-g(s) - // compute the diagonal d1,d2 - for (d1, &τ, &α, &z) in izip!(&mut self.d1, τ.iter(), α, &z[..dim1]) { - *d1 = (τ) * phi / (ζ * z) + (T::one() - α) / (z * z); - } - self.d2 = two / ζ; + let out = -self.barrier_dual(&g) - self.degree().as_T(); - // compute p, q, r where τ shares memory with q - self.p[..dim1].scalarop_from(|τi| (p0 / ζ) * τi, τ); - self.p[dim1..].scalarop_from(|zi| (p1 / ζ) * zi, &z[dim1..]); + self.data.work_pb = g; - self.q.scale(q0 / ζ); - self.r.scalarop_from(|zi| (r1 / ζ) * zi, &z[dim1..]); + out } fn barrier_dual(&mut self, z: &[T]) -> T @@ -369,29 +357,51 @@ where barrier } - fn barrier_primal(&mut self, s: &[T]) -> T - where - T: FloatT, - { - // Primal barrier: f(s) = ⟨s,g(s)⟩ - f*(-g(s)) - // NB: ⟨s,g(s)⟩ = -(dim1(K)+1) = - ν + fn higher_correction(&mut self, _η: &mut [T], _ds: &[T], _v: &[T]) { + unimplemented!() + } - // can't use "work" here because it was already - // used to construct the argument s in some cases - let mut g = std::mem::take(&mut self.work_pb); + fn update_dual_grad_H(&mut self, z: &[T]) { + let α = &self.α; + let dim1 = self.dim1(); + let data = &mut self.data; + let two: T = (2.).as_T(); - self.gradient_primal(&mut g, s); - g.negate(); //-g(s) + let phi = zip(α, z).fold(T::one(), |phi, (&αi, &zi)| phi * (zi / αi).powf(two * αi)); - let out = -self.barrier_dual(&g) - self.degree().as_T(); + let norm2w = z[dim1..].sumsq(); + let ζ = phi - norm2w; + assert!(ζ > T::zero()); - self.work_pb = g; + // compute the gradient at z + let grad = &mut data.grad; + let τ = &mut data.q; - out - } + for (τ, grad, &α, &z) in izip!(τ.iter_mut(), &mut grad[..dim1], α, &z[..dim1]) { + *τ = two * α / z; + *grad = -(*τ) * phi / ζ - (T::one() - α) / z; + } - fn higher_correction(&mut self, _η: &mut [T], _ds: &[T], _v: &[T]) { - unimplemented!() + grad[dim1..].scalarop_from(|z| (two / ζ) * z, &z[dim1..]); + + // compute Hessian information at z + let p0 = T::sqrt(phi * (phi + norm2w) / two); + let p1 = -two * phi / p0; + let q0 = T::sqrt(ζ * phi / two); + let r1 = two * T::sqrt(ζ / (phi + norm2w)); + + // compute the diagonal d1,d2 + for (d1, &τ, &α, &z) in izip!(&mut data.d1, τ.iter(), α, &z[..dim1]) { + *d1 = (τ) * phi / (ζ * z) + (T::one() - α) / (z * z); + } + data.d2 = two / ζ; + + // compute p, q, r where τ shares memory with q + data.p[..dim1].scalarop_from(|τi| (p0 / ζ) * τi, τ); + data.p[dim1..].scalarop_from(|zi| (p1 / ζ) * zi, &z[dim1..]); + + data.q.scale(q0 / ζ); + data.r.scalarop_from(|zi| (r1 / ζ) * zi, &z[dim1..]); } } @@ -406,6 +416,7 @@ where { let dim1 = self.dim1(); let two: T = (2.).as_T(); + let data = &self.data; // unscaled phi let phi = @@ -417,9 +428,9 @@ where let norm_r = r.norm(); if norm_r > T::epsilon() { - let g1 = _newton_raphson_genpowcone(norm_r, p, phi, &self.α, self.ψ); + let g1 = _newton_raphson_genpowcone(norm_r, p, phi, &self.α, data.ψ); - gr.scalarop_from(|r| (g1 / norm_r) * r, &self.r); + gr.scalarop_from(|r| (g1 / norm_r) * r, &data.r); for (gp, &α, &p) in izip!(gp.iter_mut(), &self.α, p) { *gp = -(T::one() + α + α * g1 * norm_r) / p; diff --git a/src/solver/core/cones/mod.rs b/src/solver/core/cones/mod.rs index 6374881a..f8219561 100644 --- a/src/solver/core/cones/mod.rs +++ b/src/solver/core/cones/mod.rs @@ -16,11 +16,11 @@ mod powcone; mod socone; mod zerocone; // partially specialized traits and blanket implementataions -mod exppow_common; +mod nonsymmetric_common; mod symmetric_common; //re-export everything to appear as one module -use exppow_common::*; +use nonsymmetric_common::*; pub use { compositecone::*, expcone::*, genpowcone::*, nonnegativecone::*, powcone::*, socone::*, supportedcone::*, symmetric_common::*, zerocone::*, diff --git a/src/solver/core/cones/nonnegativecone.rs b/src/solver/core/cones/nonnegativecone.rs index b7e9d7f1..bd808658 100644 --- a/src/solver/core/cones/nonnegativecone.rs +++ b/src/solver/core/cones/nonnegativecone.rs @@ -10,7 +10,7 @@ use std::iter::zip; // Nonnegative Cone // ------------------------------------- -pub struct NonnegativeCone { +pub struct NonnegativeCone { dim: usize, w: Vec, λ: Vec, diff --git a/src/solver/core/cones/exppow_common.rs b/src/solver/core/cones/nonsymmetric_common.rs similarity index 100% rename from src/solver/core/cones/exppow_common.rs rename to src/solver/core/cones/nonsymmetric_common.rs diff --git a/src/solver/core/cones/powcone.rs b/src/solver/core/cones/powcone.rs index d4025c19..4b74b4e2 100644 --- a/src/solver/core/cones/powcone.rs +++ b/src/solver/core/cones/powcone.rs @@ -8,7 +8,7 @@ use crate::{ // Power Cone // ------------------------------------- -pub struct PowerCone { +pub struct PowerCone { // power defining the cone α: T, // Hessian of the dual barrier at z @@ -223,54 +223,6 @@ where false } - fn update_dual_grad_H(&mut self, z: &[T]) { - let H = &mut self.H_dual; - let α = self.α; - let two: T = (2.).as_T(); - let four: T = (4.).as_T(); - - let phi = (z[0] / α).powf(two * α) * (z[1] / (T::one() - α)).powf(two - two * α); - let ψ = phi - z[2] * z[2]; - - // use K.grad as a temporary workspace - let gψ = &mut self.grad; - gψ[0] = two * α * phi / (z[0] * ψ); - gψ[1] = two * (T::one() - α) * phi / (z[1] * ψ); - gψ[2] = -two * z[2] / ψ; - - // compute_Hessian(K,z,H). Type is symmetric, so - // only need to assign upper triangle. - H[(0, 0)] = gψ[0] * gψ[0] - two * α * (two * α - T::one()) * phi / (z[0] * z[0] * ψ) - + (T::one() - α) / (z[0] * z[0]); - H[(0, 1)] = gψ[0] * gψ[1] - four * α * (T::one() - α) * phi / (z[0] * z[1] * ψ); - H[(1, 1)] = gψ[1] * gψ[1] - - two * (T::one() - α) * (T::one() - two * α) * phi / (z[1] * z[1] * ψ) - + α / (z[1] * z[1]); - H[(0, 2)] = gψ[0] * gψ[2]; - H[(1, 2)] = gψ[1] * gψ[2]; - H[(2, 2)] = gψ[2] * gψ[2] + two / ψ; - - // compute the gradient at z - let grad = &mut self.grad; - grad[0] = -two * α * phi / (z[0] * ψ) - (T::one() - α) / z[0]; - grad[1] = -two * (T::one() - α) * phi / (z[1] * ψ) - α / z[1]; - grad[2] = two * z[2] / ψ; - } - - fn barrier_dual(&mut self, z: &[T]) -> T - where - T: FloatT, - { - // Dual barrier: - // f*(z) = -log((z1/α)^{2α} * (z2/(1-α))^{2(1-α)} - z3*z3) - (1-α)*log(z1) - α*log(z2): - let α = self.α; - let two: T = (2.).as_T(); - let arg1 = - (z[0] / α).powf(two * α) * (z[1] / (T::one() - α)).powf(two - two * α) - z[2] * z[2]; - - -arg1.logsafe() - (T::one() - α) * z[0].logsafe() - α * z[1].logsafe() - } - fn barrier_primal(&mut self, s: &[T]) -> T where T: FloatT, @@ -294,16 +246,19 @@ where out } - // 3rd-order correction at the point z. Output is η. - // - // 3rd order correction: - // η = -0.5*[(dot(u,Hψ,v)*ψ - 2*dotψu*dotψv)/(ψ*ψ*ψ)*gψ + - // dotψu/(ψ*ψ)*Hψv + dotψv/(ψ*ψ)*Hψu - - // dotψuv/ψ + dothuv] - // where: - // Hψ = [ 2*α*(2*α-1)*ϕ/(z1*z1) 4*α*(1-α)*ϕ/(z1*z2) 0; - // 4*α*(1-α)*ϕ/(z1*z2) 2*(1-α)*(1-2*α)*ϕ/(z2*z2) 0; - // 0 0 -2;] + fn barrier_dual(&mut self, z: &[T]) -> T + where + T: FloatT, + { + // Dual barrier: + // f*(z) = -log((z1/α)^{2α} * (z2/(1-α))^{2(1-α)} - z3*z3) - (1-α)*log(z1) - α*log(z2): + let α = self.α; + let two: T = (2.).as_T(); + let arg1 = + (z[0] / α).powf(two * α) * (z[1] / (T::one() - α)).powf(two - two * α) - z[2] * z[2]; + + -arg1.logsafe() - (T::one() - α) * z[0].logsafe() - α * z[1].logsafe() + } fn higher_correction(&mut self, η: &mut [T], ds: &[T], v: &[T]) where @@ -384,6 +339,51 @@ where η[..].axpby(dotψv * inv_ψ2, Hψu, T::one()); η[..].scale((0.5).as_T()); } + + // 3rd-order correction at the point z. Output is η. + // + // 3rd order correction: + // η = -0.5*[(dot(u,Hψ,v)*ψ - 2*dotψu*dotψv)/(ψ*ψ*ψ)*gψ + + // dotψu/(ψ*ψ)*Hψv + dotψv/(ψ*ψ)*Hψu - + // dotψuv/ψ + dothuv] + // where: + // Hψ = [ 2*α*(2*α-1)*ϕ/(z1*z1) 4*α*(1-α)*ϕ/(z1*z2) 0; + // 4*α*(1-α)*ϕ/(z1*z2) 2*(1-α)*(1-2*α)*ϕ/(z2*z2) 0; + // 0 0 -2;] + + fn update_dual_grad_H(&mut self, z: &[T]) { + let H = &mut self.H_dual; + let α = self.α; + let two: T = (2.).as_T(); + let four: T = (4.).as_T(); + + let phi = (z[0] / α).powf(two * α) * (z[1] / (T::one() - α)).powf(two - two * α); + let ψ = phi - z[2] * z[2]; + + // use K.grad as a temporary workspace + let gψ = &mut self.grad; + gψ[0] = two * α * phi / (z[0] * ψ); + gψ[1] = two * (T::one() - α) * phi / (z[1] * ψ); + gψ[2] = -two * z[2] / ψ; + + // compute_Hessian(K,z,H). Type is symmetric, so + // only need to assign upper triangle. + H[(0, 0)] = gψ[0] * gψ[0] - two * α * (two * α - T::one()) * phi / (z[0] * z[0] * ψ) + + (T::one() - α) / (z[0] * z[0]); + H[(0, 1)] = gψ[0] * gψ[1] - four * α * (T::one() - α) * phi / (z[0] * z[1] * ψ); + H[(1, 1)] = gψ[1] * gψ[1] + - two * (T::one() - α) * (T::one() - two * α) * phi / (z[1] * z[1] * ψ) + + α / (z[1] * z[1]); + H[(0, 2)] = gψ[0] * gψ[2]; + H[(1, 2)] = gψ[1] * gψ[2]; + H[(2, 2)] = gψ[2] * gψ[2] + two / ψ; + + // compute the gradient at z + let grad = &mut self.grad; + grad[0] = -two * α * phi / (z[0] * ψ) - (T::one() - α) / z[0]; + grad[1] = -two * (T::one() - α) * phi / (z[1] * ψ) - α / z[1]; + grad[2] = two * z[2] / ψ; + } } impl Nonsymmetric3DCone for PowerCone diff --git a/src/solver/core/cones/psdtrianglecone.rs b/src/solver/core/cones/psdtrianglecone.rs index f1f96de8..26e490f9 100644 --- a/src/solver/core/cones/psdtrianglecone.rs +++ b/src/solver/core/cones/psdtrianglecone.rs @@ -8,7 +8,7 @@ use crate::{ // Positive Semidefinite Cone (Scaled triangular form) // ------------------------------------ -pub struct PSDConeWork { +pub struct PSDConeData { cholS: CholeskyEngine, cholZ: CholeskyEngine, SVD: SVDEngine, @@ -28,7 +28,7 @@ pub struct PSDConeWork { workvec: Vec, } -impl PSDConeWork +impl PSDConeData where T: FloatT, { @@ -59,9 +59,9 @@ where } pub struct PSDTriangleCone { - n: usize, // matrix dimension, i.e. matrix is n /times n - numel: usize, // total number of elements (lower triangle of) the matrix - work: Box>, // Boxed so that the PSDCone enum_dispatch variant isn't huge + n: usize, // matrix dimension, i.e. matrix is n × n + numel: usize, // total number of elements in (lower triangle of) the matrix + data: Box>, // Boxed so that the PSDCone enum_dispatch variant isn't huge } impl PSDTriangleCone @@ -73,7 +73,7 @@ where Self { n, numel: triangular_number(n), - work: Box::new(PSDConeWork::::new(n)), + data: Box::new(PSDConeData::::new(n)), } } } @@ -94,6 +94,10 @@ where true } + fn allows_primal_dual_scaling(&self) -> bool { + true + } + fn rectify_equilibration(&self, δ: &mut [T], e: &[T]) -> bool { δ.copy_from(e).recip().scale(e.mean()); true // scalar equilibration @@ -108,10 +112,10 @@ where α = T::max_value(); e = &[T::zero(); 0]; } else { - let Z = &mut self.work.workmat1; + let Z = &mut self.data.workmat1; _svec_to_mat(Z, z); - self.work.Eig.eigvals(Z).expect("Eigval error"); - e = &self.work.Eig.λ; + self.data.Eig.eigvals(Z).expect("Eigval error"); + e = &self.data.Eig.λ; α = e.minimum(); } @@ -135,9 +139,9 @@ where } fn set_identity_scaling(&mut self) { - self.work.R.set_identity(); - self.work.Rinv.set_identity(); - self.work.Hs.set_identity(); + self.data.R.set_identity(); + self.data.Rinv.set_identity(); + self.data.Hs.set_identity(); } fn update_scaling( @@ -152,7 +156,7 @@ where return true; } - let f = &mut self.work; + let f = &mut self.data; let (S, Z) = (&mut f.workmat1, &mut f.workmat2); _svec_to_mat(S, s); _svec_to_mat(Z, z); @@ -204,7 +208,7 @@ where } fn get_Hs(&self, Hsblock: &mut [T]) { - self.work.Hs.sym().pack_triu(Hsblock); + self.data.Hs.sym().pack_triu(Hsblock); } fn mul_Hs(&mut self, y: &mut [T], x: &[T], work: &mut [T]) { @@ -216,7 +220,7 @@ where fn affine_ds(&self, ds: &mut [T], _s: &[T]) { ds.set(T::zero()); for k in 0..self.n { - ds[triangular_index(k)] = self.work.λ[k] * self.work.λ[k]; + ds[triangular_index(k)] = self.data.λ[k] * self.data.λ[k]; } } @@ -237,9 +241,9 @@ where _settings: &CoreSettings, αmax: T, ) -> (T, T) { - let Λisqrt = &self.work.Λisqrt; - let d = &mut self.work.workvec; - let engine = &mut self.work.Eig; + let Λisqrt = &self.data.Λisqrt; + let d = &mut self.data.workvec; + let engine = &mut self.data.Eig; // d = Δz̃ = WΔz _mul_Wx_inner( @@ -248,12 +252,12 @@ where dz, T::one(), T::zero(), - &self.work.R, - &mut self.work.workmat1, - &mut self.work.workmat2, - &mut self.work.workmat3, + &self.data.R, + &mut self.data.workmat1, + &mut self.data.workmat2, + &mut self.data.workmat3, ); - let workΔ = &mut self.work.workmat1; + let workΔ = &mut self.data.workmat1; let αz = _step_length_psd_component(workΔ, engine, d, Λisqrt, αmax); // d = Δs̃ = W^{-T}Δs @@ -263,18 +267,18 @@ where ds, T::one(), T::zero(), - &self.work.Rinv, - &mut self.work.workmat1, - &mut self.work.workmat2, - &mut self.work.workmat3, + &self.data.Rinv, + &mut self.data.workmat1, + &mut self.data.workmat2, + &mut self.data.workmat3, ); - let workΔ = &mut self.work.workmat1; + let workΔ = &mut self.data.workmat1; let αs = _step_length_psd_component(workΔ, engine, d, Λisqrt, αmax); (αz, αs) } - fn compute_barrier(&self, _z: &[T], _s: &[T], _dz: &[T], _ds: &[T], _α: T) -> T { + fn compute_barrier(&mut self, _z: &[T], _s: &[T], _dz: &[T], _ds: &[T], _α: T) -> T { // We should return this, but in a smarter way. // This is not yet implemented, but would only // be required for problems mixing PSD and @@ -295,13 +299,13 @@ where { // implements x = λ \ z for the SDP cone fn λ_inv_circ_op(&mut self, x: &mut [T], z: &[T]) { - let X = &mut self.work.workmat1; - let Z = &mut self.work.workmat2; + let X = &mut self.data.workmat1; + let Z = &mut self.data.workmat2; _svec_to_mat(X, x); _svec_to_mat(Z, z); - let λ = &self.work.λ; + let λ = &self.data.λ; let two: T = (2.).as_T(); for i in 0..self.n { for j in 0..self.n { @@ -318,10 +322,10 @@ where x, α, β, - &self.work.R, - &mut self.work.workmat1, - &mut self.work.workmat2, - &mut self.work.workmat3, + &self.data.R, + &mut self.data.workmat1, + &mut self.data.workmat2, + &mut self.data.workmat3, ) } @@ -332,10 +336,10 @@ where x, α, β, - &self.work.Rinv, - &mut self.work.workmat1, - &mut self.work.workmat2, - &mut self.work.workmat3, + &self.data.Rinv, + &mut self.data.workmat1, + &mut self.data.workmat2, + &mut self.data.workmat3, ) } } @@ -385,9 +389,9 @@ where { fn circ_op(&mut self, x: &mut [T], y: &[T], z: &[T]) { let (Y, Z, X) = ( - &mut self.work.workmat1, - &mut self.work.workmat2, - &mut self.work.workmat3, + &mut self.data.workmat1, + &mut self.data.workmat2, + &mut self.data.workmat3, ); _svec_to_mat(Y, y); _svec_to_mat(Z, z); diff --git a/src/solver/core/cones/socone.rs b/src/solver/core/cones/socone.rs index 2f7e28ba..10d000a1 100644 --- a/src/solver/core/cones/socone.rs +++ b/src/solver/core/cones/socone.rs @@ -8,7 +8,7 @@ use crate::{ // Second order Cone // ------------------------------------- -pub struct SecondOrderCone { +pub struct SecondOrderCone { dim: usize, //internal working variables for W and its products w: Vec, diff --git a/src/solver/core/cones/supportedcone.rs b/src/solver/core/cones/supportedcone.rs index 21646324..139f6b21 100644 --- a/src/solver/core/cones/supportedcone.rs +++ b/src/solver/core/cones/supportedcone.rs @@ -25,6 +25,10 @@ pub enum SupportedConeT { /// /// The parameter indicates the cones dimension. SecondOrderConeT(usize), + /// The exponential cone in R^3. + /// + /// This cone takes no parameters + ExponentialConeT(), /// The power cone in R^3. /// /// The parameter indicates the power. @@ -33,10 +37,7 @@ pub enum SupportedConeT { /// /// The parameter indicates the power and dimensions. GenPowerConeT(Vec, usize), - /// The exponential cone in R^3. - /// - /// This cone takes no parameters - ExponentialConeT(), + /// The positive semidefinite cone in triangular form. /// /// The parameter indicates the matrix dimension, i.e. size = n @@ -84,11 +85,11 @@ pub fn make_cone(cone: &SupportedConeT) -> SupportedCone { SupportedConeT::SecondOrderConeT(dim) => SecondOrderCone::::new(*dim).into(), SupportedConeT::ExponentialConeT() => ExponentialCone::::new().into(), SupportedConeT::PowerConeT(α) => PowerCone::::new(*α).into(), - #[cfg(feature = "sdp")] - SupportedConeT::PSDTriangleConeT(dim) => PSDTriangleCone::::new(dim).into(), SupportedConeT::GenPowerConeT(α, dim2) => { GenPowerCone::::new((*α).clone(), *dim2).into() } + #[cfg(feature = "sdp")] + SupportedConeT::PSDTriangleConeT(dim) => PSDTriangleCone::::new(*dim).into(), } } @@ -109,17 +110,13 @@ where SecondOrderCone(SecondOrderCone), ExponentialCone(ExponentialCone), PowerCone(PowerCone), + GenPowerCone(GenPowerCone), #[cfg(feature = "sdp")] PSDTriangleCone(PSDTriangleCone), - GenPowerCone(GenPowerCone), } -// we put PSDTriangleCone in a Box above since it is by the -// largest enum variant. We need some auto dereferencing -// for it so that it behaves like the other variants - // ------------------------------------- -// Finally, we need a tagging enum with no data fields to act +// we need a tagging enum with no data fields to act // as a bridge between the SupportedConeT API types and the // internal SupportedCone enum_dispatch wrapper. This enum // has no data attached at all, so we can just convert to a u8. @@ -138,9 +135,9 @@ pub(crate) enum SupportedConeTag { SecondOrderCone, ExponentialCone, PowerCone, + GenPowerCone, #[cfg(feature = "sdp")] PSDTriangleCone, - GenPowerCone, } pub(crate) trait SupportedConeAsTag { diff --git a/src/solver/core/cones/zerocone.rs b/src/solver/core/cones/zerocone.rs index b83fe006..9f4cca01 100644 --- a/src/solver/core/cones/zerocone.rs +++ b/src/solver/core/cones/zerocone.rs @@ -9,7 +9,7 @@ use core::marker::PhantomData; // Zero Cone // ------------------------------------- -pub struct ZeroCone { +pub struct ZeroCone { dim: usize, phantom: PhantomData, } diff --git a/src/solver/core/kktsolvers/direct/quasidef/datamap.rs b/src/solver/core/kktsolvers/direct/quasidef/datamap.rs deleted file mode 100644 index a3ab149e..00000000 --- a/src/solver/core/kktsolvers/direct/quasidef/datamap.rs +++ /dev/null @@ -1,95 +0,0 @@ -#![allow(non_snake_case)] - -use crate::algebra::*; -use crate::solver::core::cones::*; - -use super::*; - -pub struct LDLDataMap { - pub P: Vec, - pub A: Vec, - pub Hsblocks: Vec, //indices of the lower RHS blocks (by cone) - pub SOC_u: Vec>, //off diag dense columns u - pub SOC_v: Vec>, //off diag dense columns v - pub SOC_D: Vec, //diag of just the sparse SOC expansion D - pub GenPow_p: Vec>, // off diag dense columns p - pub GenPow_q: Vec>, // off diag dense columns q - pub GenPow_r: Vec>, // off diag dense columns r - pub GenPow_D: Vec, // diag of just the sparse GenPow expansion D - - // all of above terms should be disjoint and their union - // should cover all of the user data in the KKT matrix. Now - // we make two last redundant indices that will tell us where - // the whole diagonal is, including structural zeros. - pub diagP: Vec, - pub diag_full: Vec, -} - -impl LDLDataMap { - pub fn new( - Pmat: &CscMatrix, - Amat: &CscMatrix, - cones: &CompositeCone, - ) -> Self { - let (m, n) = (Amat.nrows(), Pmat.nrows()); - let P = vec![0; Pmat.nnz()]; - let A = vec![0; Amat.nnz()]; - - // the diagonal of the ULHS KKT block P. - // NB : we fill in structural zeros here even if the matrix - // P is empty (e.g. as in an LP), so we can have entries in - // index Pdiag that are not present in the index P - let diagP = vec![0; n]; - - // make an index for each of the Hs blocks for each cone - let Hsblocks = allocate_kkt_Hsblocks::(cones); - - // now do the SOC and generalized power expansion pieces - let nsoc = cones.type_count(SupportedConeTag::SecondOrderCone); - let psoc = 2 * nsoc; - let SOC_D = vec![0; psoc]; - - let mut SOC_u = Vec::>::with_capacity(nsoc); - let mut SOC_v = Vec::>::with_capacity(nsoc); - - let ngenpow = cones.type_count(SupportedConeTag::GenPowerCone); - let pgenpow = 3 * ngenpow; - let GenPow_D = vec![0; pgenpow]; - - let mut GenPow_p = Vec::>::with_capacity(ngenpow); - let mut GenPow_q = Vec::>::with_capacity(ngenpow); - let mut GenPow_r = Vec::>::with_capacity(ngenpow); - - for cone in cones.iter() { - // `cone` here will be of our SupportedCone enum wrapper, so - // we see if we can extract a SecondOrderCone `soc` - if let SupportedCone::SecondOrderCone(soc) = cone { - SOC_u.push(vec![0; soc.numel()]); - SOC_v.push(vec![0; soc.numel()]); - } - // Generalized power cones - if let SupportedCone::GenPowerCone(genpow) = cone { - GenPow_p.push(vec![0; genpow.numel()]); - GenPow_q.push(vec![0; genpow.dim1()]); - GenPow_r.push(vec![0; genpow.dim2()]); - } - } - - let diag_full = vec![0; m + n + psoc + pgenpow]; - - Self { - P, - A, - Hsblocks, - SOC_u, - SOC_v, - SOC_D, - GenPow_p, - GenPow_q, - GenPow_r, - GenPow_D, - diagP, - diag_full, - } - } -} diff --git a/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs b/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs new file mode 100644 index 00000000..961db4d5 --- /dev/null +++ b/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs @@ -0,0 +1,403 @@ +#![allow(non_snake_case)] + +use super::*; +use crate::solver::core::cones::*; +use enum_dispatch::*; + +#[enum_dispatch(SparseExpansionMapTrait)] +pub(crate) enum SparseExpansionMap { + SOCExpansionMap(SOCExpansionMap), + GenPowExpansionMap(GenPowExpansionMap), +} + +#[enum_dispatch(SparseExpansionConeTrait)] +pub(crate) enum SparseExpansionCone<'a, T> +where + T: FloatT, +{ + SecondOrderCone(&'a SecondOrderCone), + GenPowerCone(&'a GenPowerCone), +} + +impl<'a, T> SupportedCone +where + T: FloatT, +{ + pub(crate) fn to_sparse(&'a self) -> Option> { + match self { + SupportedCone::SecondOrderCone(sc) => Some(SparseExpansionCone::SecondOrderCone(sc)), + SupportedCone::GenPowerCone(sc) => Some(SparseExpansionCone::GenPowerCone(sc)), + _ => None, + } + } + fn is_sparsifiable(&self) -> bool { + self.to_sparse().is_some() + } +} + +#[enum_dispatch] +pub(crate) trait SparseExpansionMapTrait { + fn pdim(&self) -> usize; + fn nnz_vec(&self) -> usize; + fn Dsigns(&self) -> &[i8]; +} + +impl SparseExpansionMapTrait for Vec { + fn pdim(&self) -> usize { + self.iter().fold(0, |pdim, map| pdim + map.pdim()) + } + fn nnz_vec(&self) -> usize { + self.iter().fold(0, |nnz, map| nnz + map.nnz_vec()) + } + fn Dsigns(&self) -> &[i8] { + unreachable!() + } +} + +type UpdateFcn = fn(&mut BoxedDirectLDLSolver, &mut CscMatrix, &[usize], &[T]) -> (); +type ScaleFcn = fn(&mut BoxedDirectLDLSolver, &mut CscMatrix, &[usize], T) -> (); +#[enum_dispatch] +pub(crate) trait SparseExpansionConeTrait +where + T: FloatT, +{ + fn expansion_map(&self) -> SparseExpansionMap; + fn csc_colcount_sparsecone( + &self, + map: &SparseExpansionMap, + K: &mut CscMatrix, + row: usize, + col: usize, + shape: MatrixTriangle, + ); + fn csc_fill_sparsecone( + &self, + map: &mut SparseExpansionMap, + K: &mut CscMatrix, + row: usize, + col: usize, + shape: MatrixTriangle, + ); + fn csc_update_sparsecone( + &self, + map: &SparseExpansionMap, + ldl: &mut BoxedDirectLDLSolver, + K: &mut CscMatrix, + updateFcn: UpdateFcn, + scaleFcn: ScaleFcn, + ); +} + +macro_rules! impl_map_recover { + ($CONE:ident,$MAP:ident) => { + impl<'a, T> $CONE { + pub(crate) fn recover_map(&self, map: &'a SparseExpansionMap) -> &'a $MAP { + match map { + SparseExpansionMap::$MAP(map) => map, + _ => panic!(), + } + } + pub(crate) fn recover_map_mut(&self, map: &'a mut SparseExpansionMap) -> &'a mut $MAP { + match map { + SparseExpansionMap::$MAP(map) => map, + _ => panic!(), + } + } + } + }; +} + +//-------------------------------------- +// Second order cone data map +//-------------------------------------- + +pub(crate) struct SOCExpansionMap { + u: Vec, //off diag dense columns u + v: Vec, //off diag dense columns v + D: [usize; 2], //diag D +} + +impl SOCExpansionMap { + pub fn new(cone: &SecondOrderCone) -> Self { + let u = vec![0; cone.numel()]; + let v = vec![0; cone.numel()]; + let D = [0; 2]; + Self { u, v, D } + } +} + +impl SparseExpansionMapTrait for SOCExpansionMap { + fn pdim(&self) -> usize { + 2 + } + fn nnz_vec(&self) -> usize { + 2 * self.v.len() + } + fn Dsigns(&self) -> &[i8] { + &[-1, 1] + } +} + +impl_map_recover!(SecondOrderCone, SOCExpansionMap); + +impl<'a, T> SparseExpansionConeTrait for &'a SecondOrderCone +where + T: FloatT, +{ + fn expansion_map(&self) -> SparseExpansionMap { + SparseExpansionMap::SOCExpansionMap(SOCExpansionMap::new(&self)) + } + + fn csc_colcount_sparsecone( + &self, + map: &SparseExpansionMap, + K: &mut CscMatrix, + row: usize, + col: usize, + shape: MatrixTriangle, + ) { + let map = self.recover_map(&map); + let nvars = self.numel(); + + match shape { + MatrixTriangle::Triu => { + K.colcount_colvec(nvars, row, col); // u column + K.colcount_colvec(nvars, row, col + 1); // v column + } + MatrixTriangle::Tril => { + K.colcount_rowvec(nvars, col, row); // u row + K.colcount_rowvec(nvars, col + 1, row); // v row + } + } + K.colcount_diag(col, map.pdim()); + } + + fn csc_fill_sparsecone( + &self, + map: &mut SparseExpansionMap, + K: &mut CscMatrix, + row: usize, + col: usize, + shape: MatrixTriangle, + ) { + let map = self.recover_map_mut(map); + + // fill structural zeros for u and v columns for this cone + // note v is the first extra row/column, u is second + match shape { + MatrixTriangle::Triu => { + K.fill_colvec(&mut map.v, row, col); //u + K.fill_colvec(&mut map.u, row, col + 1); //v + } + MatrixTriangle::Tril => { + K.fill_rowvec(&mut map.v, col, row); //u + K.fill_rowvec(&mut map.u, col + 1, row); //v + } + } + let pdim = map.pdim(); + K.fill_diag(&mut map.D, col, pdim); + } + + fn csc_update_sparsecone( + &self, + map: &SparseExpansionMap, + ldl: &mut BoxedDirectLDLSolver, + K: &mut CscMatrix, + updateFcn: UpdateFcn, + scaleFcn: ScaleFcn, + ) { + let map = self.recover_map(map); + let η2 = self.η * self.η; + + // off diagonal columns (or rows) + updateFcn(ldl, K, &map.u, &self.u); + updateFcn(ldl, K, &map.v, &self.v); + scaleFcn(ldl, K, &map.u, -η2); + scaleFcn(ldl, K, &map.v, -η2); + + //set diagonal to η^2*(-1,1) in the extended rows/cols + updateFcn(ldl, K, &map.D, &[-η2, η2]); + } +} + +//-------------------------------------- +// Generalized power cone data map +//-------------------------------------- + +pub(crate) struct GenPowExpansionMap { + p: Vec, //off diag dense columns p + q: Vec, //off diag dense columns q + r: Vec, //off diag dense columns r + D: [usize; 3], //diag D +} + +impl GenPowExpansionMap { + pub fn new(cone: &GenPowerCone) -> Self { + let p = vec![0; cone.numel()]; + let q = vec![0; cone.dim1()]; + let r = vec![0; cone.dim2()]; + let D = [0; 3]; + Self { p, q, r, D } + } +} + +impl SparseExpansionMapTrait for GenPowExpansionMap { + fn pdim(&self) -> usize { + 3 + } + fn nnz_vec(&self) -> usize { + self.p.len() + self.q.len() + self.r.len() + } + fn Dsigns(&self) -> &[i8] { + &[-1, -1, 1] + } +} + +impl_map_recover!(GenPowerCone, GenPowExpansionMap); + +impl<'a, T> SparseExpansionConeTrait for &'a GenPowerCone +where + T: FloatT, +{ + fn expansion_map(&self) -> SparseExpansionMap { + SparseExpansionMap::GenPowExpansionMap(GenPowExpansionMap::new(&self)) + } + + fn csc_colcount_sparsecone( + &self, + map: &SparseExpansionMap, + K: &mut CscMatrix, + row: usize, + col: usize, + shape: MatrixTriangle, + ) { + let map = self.recover_map(&map); + let nvars = self.numel(); + let dim1 = self.dim1(); + let dim2 = self.dim2(); + + match shape { + MatrixTriangle::Triu => { + K.colcount_colvec(dim1, row, col); //q column + K.colcount_colvec(dim2, row + dim1, col + 1); //r column + K.colcount_colvec(nvars, row, col + 2); //p column + } + MatrixTriangle::Tril => { + K.colcount_rowvec(dim1, col, row); //q row + K.colcount_rowvec(dim2, col + 1, row + dim1); //r row + K.colcount_rowvec(nvars, col + 2, row); //p row + } + } + K.colcount_diag(col, map.pdim()); + } + + fn csc_fill_sparsecone( + &self, + map: &mut SparseExpansionMap, + K: &mut CscMatrix, + row: usize, + col: usize, + shape: MatrixTriangle, + ) { + let map = self.recover_map_mut(map); + let dim1 = self.dim1(); + + match shape { + MatrixTriangle::Triu => { + K.fill_colvec(&mut map.q, row, col); //q column + K.fill_colvec(&mut map.r, row + dim1, col + 1); //r column + K.fill_colvec(&mut map.p, row, col + 2); //p column + } + MatrixTriangle::Tril => { + K.fill_rowvec(&mut map.q, col, row); //q row + K.fill_rowvec(&mut map.r, col + 1, row + dim1); //r row + K.fill_rowvec(&mut map.p, col + 2, row); //p row + } + } + K.colcount_diag(col, map.pdim()); + } + + fn csc_update_sparsecone( + &self, + map: &SparseExpansionMap, + ldl: &mut BoxedDirectLDLSolver, + K: &mut CscMatrix, + updateFcn: UpdateFcn, + scaleFcn: ScaleFcn, + ) { + let map = self.recover_map(&map); + let data = &self.data; + let sqrtμ = data.μ.sqrt(); + + //&off diagonal columns (or rows), distribute √μ to off-diagonal terms + updateFcn(ldl, K, &map.q, &data.q); + updateFcn(ldl, K, &map.r, &data.r); + updateFcn(ldl, K, &map.p, &data.p); + scaleFcn(ldl, K, &map.q, -sqrtμ); + scaleFcn(ldl, K, &map.r, -sqrtμ); + scaleFcn(ldl, K, &map.p, -sqrtμ); + + //&normalize diagonal terms to 1/-1 in the extended rows/cols + updateFcn(ldl, K, &map.D, &[-T::one(), -T::one(), T::one()]); + } +} + +//-------------------------------------- +// LDL Data Map +//-------------------------------------- + +pub(crate) struct LDLDataMap { + pub P: Vec, + pub A: Vec, + pub Hsblocks: Vec, //indices of the lower RHS blocks (by cone) + pub sparse_maps: Vec, //sparse cone expansion terms + + // all of above terms should be disjoint and their union + // should cover all of the user data in the KKT matrix. Now + // we make two last redundant indices that will tell us where + // the whole diagonal is, including structural zeros. + pub diagP: Vec, + pub diag_full: Vec, +} + +impl LDLDataMap { + pub fn new( + Pmat: &CscMatrix, + Amat: &CscMatrix, + cones: &CompositeCone, + ) -> Self { + let (m, n) = (Amat.nrows(), Pmat.nrows()); + let P = vec![0; Pmat.nnz()]; + let A = vec![0; Amat.nnz()]; + + // the diagonal of the ULHS KKT block P. + // NB : we fill in structural zeros here even if the matrix + // P is empty (e.g. as in an LP), so we can have entries in + // index Pdiag that are not present in the index P + let diagP = vec![0; n]; + + // make an index for each of the Hs blocks for each cone + let Hsblocks = allocate_kkt_Hsblocks::(cones); + + // now do the sparse cone expansion pieces + let nsparse = cones.iter().filter(|&c| c.is_sparsifiable()).count(); + let mut sparse_maps = Vec::with_capacity(nsparse); + + for cone in cones.iter() { + if let Some(sc) = cone.to_sparse() { + sparse_maps.push(sc.expansion_map()); + } + } + + let diag_full = vec![0; m + n + sparse_maps.pdim()]; + + Self { + P, + A, + Hsblocks, + sparse_maps, + diagP, + diag_full, + } + } +} diff --git a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs index d7d00949..b7d410db 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs @@ -13,14 +13,13 @@ use std::iter::zip; // We require Send here to allow pyo3 builds to share // solver objects between threads. -type BoxedDirectLDLSolver = Box + Send>; +pub(crate) type BoxedDirectLDLSolver = Box + Send>; pub struct DirectLDLKKTSolver { // problem dimensions m: usize, n: usize, - p_soc: usize, - p_genpow: usize, + p: usize, // Left and right hand sides for solves x: Vec, @@ -63,32 +62,30 @@ where n: usize, settings: &CoreSettings, ) -> Self { - // solving in sparse format. Need this many - // extra variables for SOCs - let p_soc = 2 * cones.type_count(SupportedConeTag::SecondOrderCone); - let p_genpow = 3 * cones.type_count(SupportedConeTag::GenPowerCone); + // get a constructor for the LDL solver we should use, + // and also the matrix shape it requires + let (kktshape, ldl_ctor) = _get_ldlsolver_config(settings); + + //construct a KKT matrix of the right shape + let (KKT, map) = assemble_kkt_matrix(P, A, cones, kktshape); + + //Need this many extra variables for sparse cones + let p = map.sparse_maps.pdim(); // LHS/RHS/work for iterative refinement - let x = vec![T::zero(); n + m + p_soc + p_genpow]; - let b = vec![T::zero(); n + m + p_soc + p_genpow]; - let work1 = vec![T::zero(); n + m + p_soc + p_genpow]; - let work2 = vec![T::zero(); n + m + p_soc + p_genpow]; + let x = vec![T::zero(); n + m + p]; + let b = vec![T::zero(); n + m + p]; + let work1 = vec![T::zero(); n + m + p]; + let work2 = vec![T::zero(); n + m + p]; // the expected signs of D in LDL - let mut dsigns = vec![1_i8; n + m + p_soc + p_genpow]; - _fill_signs(&mut dsigns, m, n, p_soc, p_genpow); + let mut dsigns = vec![1_i8; n + m + p]; + _fill_signs(&mut dsigns, m, n, &map); // updates to the diagonal of KKT will be // assigned here before updating matrix entries let Hsblocks = allocate_kkt_Hsblocks::(cones); - // get a constructor for the LDL solver we should use, - // and also the matrix shape it requires - let (kktshape, ldl_ctor) = _get_ldlsolver_config(settings); - - //construct a KKT matrix of the right shape - let (KKT, map) = assemble_kkt_matrix(P, A, cones, kktshape); - let diagonal_regularizer = T::zero(); // now make the LDL linear solver engine @@ -97,8 +94,7 @@ where Self { m, n, - p_soc, - p_genpow, + p, x, b, work1, @@ -128,71 +124,26 @@ where values.negate(); _update_values(&mut self.ldlsolver, &mut self.KKT, index, values); - // SOC: update the scaled u and v columns. - let mut cidx = 0; // which of the SOCs are we working on? - - for cone in cones.iter() { - // `cone` here will be of our SupportedCone enum wrapper, so - // we can extract a SecondOrderCone `soc` - if let SupportedCone::SecondOrderCone(soc) = cone { - let η2 = T::powi(soc.η, 2); - - //off diagonal columns (or rows)s - let KKT = &mut self.KKT; - let ldlsolver = &mut self.ldlsolver; - - _update_values(ldlsolver, KKT, &map.SOC_u[cidx], &soc.u); - _update_values(ldlsolver, KKT, &map.SOC_v[cidx], &soc.v); - _scale_values(ldlsolver, KKT, &map.SOC_u[cidx], -η2); - _scale_values(ldlsolver, KKT, &map.SOC_v[cidx], -η2); - - //add η^2*(-1/1) to diagonal in the extended rows/cols - _update_values(ldlsolver, KKT, &[map.SOC_D[cidx * 2]], &[-η2; 1]); - _update_values(ldlsolver, KKT, &[map.SOC_D[cidx * 2 + 1]], &[η2; 1]); - - cidx += 1; - } //end match - } //end for - - // GenPow: update the scaled p,q,r columns. - let mut cidx = 0; // which of the GenPows are we working on? + let mut sparse_map_iter = map.sparse_maps.iter(); + let ldl = &mut self.ldlsolver; + let KKT = &mut self.KKT; for cone in cones.iter() { - // `cone` here will be of our SupportedCone enum wrapper, so - // we can extract a GenPowerCone `genpow` - if let SupportedCone::GenPowerCone(genpow) = cone { - let minus_sqrtμ = -genpow.μ.sqrt(); - - //off diagonal columns (or rows)s - let KKT = &mut self.KKT; - let ldlsolver = &mut self.ldlsolver; - - _update_values(ldlsolver, KKT, &map.GenPow_q[cidx], &genpow.q); - _update_values(ldlsolver, KKT, &map.GenPow_r[cidx], &genpow.r); - _update_values(ldlsolver, KKT, &map.GenPow_p[cidx], &genpow.p); - _scale_values(ldlsolver, KKT, &map.GenPow_q[cidx], minus_sqrtμ); - _scale_values(ldlsolver, KKT, &map.GenPow_r[cidx], minus_sqrtμ); - _scale_values(ldlsolver, KKT, &map.GenPow_p[cidx], minus_sqrtμ); - - //add η^2*(-1/1) to diagonal in the extended rows/cols - // YC: Is it a bug in SOC implementation? - _update_values(ldlsolver, KKT, &[map.GenPow_D[cidx * 3]], &[-T::one()]); - _update_values(ldlsolver, KKT, &[map.GenPow_D[cidx * 3 + 1]], &[-T::one()]); - _update_values(ldlsolver, KKT, &[map.GenPow_D[cidx * 3 + 2]], &[T::one()]); - - cidx += 1; - } //end match - } //end for + if let Some(sc) = cone.to_sparse() { + let thismap = sparse_map_iter.next().unwrap(); + sc.csc_update_sparsecone(thismap, ldl, KKT, _update_values, _scale_values); + } + } self.regularize_and_refactor(settings) } //end fn fn setrhs(&mut self, rhsx: &[T], rhsz: &[T]) { - let (m, n, p, p_genpow) = (self.m, self.n, self.p_soc, self.p_genpow); + let (m, n, p) = (self.m, self.n, self.p); self.b[0..n].copy_from(rhsx); self.b[n..(n + m)].copy_from(rhsz); - self.b[n + m..(n + m + p + p_genpow)].fill(T::zero()); + self.b[n + m..(n + m + p)].fill(T::zero()); } fn solve( @@ -438,28 +389,17 @@ fn _scale_values_KKT(KKT: &mut CscMatrix, index: &[usize], scale: } } -fn _fill_signs(signs: &mut [i8], m: usize, n: usize, p_soc: usize, p_genpow: usize) { +fn _fill_signs(signs: &mut [i8], m: usize, n: usize, map: &LDLDataMap) { signs.fill(1); //flip expected negative signs of D in LDL signs[n..(n + m)].iter_mut().for_each(|x| *x = -*x); - //the trailing block of p_soc entries should - //have alternating signs - signs[(n + m)..(n + m + p_soc)] - .iter_mut() - .step_by(2) - .for_each(|x| *x = -*x); - - //the trailing block of p_genpow entries should - //have two - signs and one + sign alternatively - signs[(n + m + p_soc)..(n + m + p_soc + p_genpow)] - .iter_mut() - .step_by(3) - .for_each(|x| *x = -*x); - signs[(n + m + p_soc)..(n + m + p_soc + p_genpow)] - .iter_mut() - .skip(1) - .step_by(3) - .for_each(|x| *x = -*x); + let mut p = m + n; + // assign D signs for sparse expansion cones + for thismap in map.sparse_maps.iter() { + let thisp = thismap.pdim(); + signs[p..(p + thisp)].copy_from_slice(thismap.Dsigns()); + p += thisp; + } } diff --git a/src/solver/core/kktsolvers/direct/quasidef/kkt_assembly.rs b/src/solver/core/kktsolvers/direct/quasidef/kkt_assembly.rs new file mode 100644 index 00000000..dbd8d272 --- /dev/null +++ b/src/solver/core/kktsolvers/direct/quasidef/kkt_assembly.rs @@ -0,0 +1,264 @@ +#![allow(non_snake_case)] + +use super::datamaps::*; +use crate::algebra::*; +use crate::solver::core::cones::CompositeCone; +use crate::solver::core::cones::*; +use num_traits::Zero; + +pub(crate) fn allocate_kkt_Hsblocks(cones: &CompositeCone) -> Vec +where + T: FloatT, + Z: Zero + Clone, +{ + let mut nnz = 0; + if let Some(rng_last) = cones.rng_blocks.last() { + nnz = rng_last.end; + } + vec![Z::zero(); nnz] +} + +pub(crate) fn assemble_kkt_matrix( + P: &CscMatrix, + A: &CscMatrix, + cones: &CompositeCone, + shape: MatrixTriangle, +) -> (CscMatrix, LDLDataMap) { + let mut map = LDLDataMap::new(P, A, cones); + let (m, n) = A.size(); + let p = map.sparse_maps.pdim(); + + // entries actually on the diagonal of P + let nnz_diagP = P.count_diagonal_entries(); + + // total entries in the Hs blocks + let nnz_Hsblocks = map.Hsblocks.len(); + + let nnzKKT = P.nnz() + // Number of elements in P + n - // Number of elements in diagonal top left block + nnz_diagP + // remove double count on the diagonal if P has entries + A.nnz() + // Number of nonzeros in A + nnz_Hsblocks + // Number of elements in diagonal below A' + map.sparse_maps.nnz_vec() + // Number of elements in sparse cone off diagonals + p; //Number of elements in diagonal of sparse cones + + let mut K = CscMatrix::::spalloc((m + n + p, m + n + p), nnzKKT); + + _kkt_assemble_colcounts(&mut K, P, A, cones, &map, shape); + _kkt_assemble_fill(&mut K, P, A, cones, &mut map, shape); + + (K, map) +} +fn _kkt_assemble_colcounts( + K: &mut CscMatrix, + P: &CscMatrix, + A: &CscMatrix, + cones: &CompositeCone, + map: &LDLDataMap, + shape: MatrixTriangle, +) { + let (m, n) = A.size(); + + // use K.p to hold nnz entries in each + // column of the KKT matrix + K.colptr.fill(0); + + match shape { + MatrixTriangle::Triu => { + K.colcount_block(P, 0, MatrixShape::N); + K.colcount_missing_diag(P, 0); + K.colcount_block(A, n, MatrixShape::T); + } + MatrixTriangle::Tril => { + K.colcount_missing_diag(P, 0); + K.colcount_block(P, 0, MatrixShape::T); + K.colcount_block(A, 0, MatrixShape::N); + } + } + + // track the next sparse column to fill (assuming triu fill) + let mut pcol = m + n; //next sparse column to fill + let mut sparse_map_iter = map.sparse_maps.iter(); + + for (i, cone) in cones.iter().enumerate() { + let row = cones.rng_cones[i].start + n; + + // add the Hs blocks in the lower right + let blockdim = cone.numel(); + if cone.Hs_is_diagonal() { + K.colcount_diag(row, blockdim); + } else { + K.colcount_dense_triangle(row, blockdim, shape); + } + + //add sparse expansions columns for sparse cones + if let Some(sc) = cone.to_sparse() { + let thismap = sparse_map_iter.next().unwrap(); + sc.csc_colcount_sparsecone(thismap, K, row, pcol, shape); + pcol += thismap.pdim(); + } + } +} + +fn _kkt_assemble_fill( + K: &mut CscMatrix, + P: &CscMatrix, + A: &CscMatrix, + cones: &CompositeCone, + map: &mut LDLDataMap, + shape: MatrixTriangle, +) { + let (m, n) = A.size(); + + // cumsum total entries to convert to K.p + K.colcount_to_colptr(); + + match shape { + MatrixTriangle::Triu => { + K.fill_block(P, &mut map.P, 0, 0, MatrixShape::N); + K.fill_missing_diag(P, 0); // after adding P, since triu form + // fill in value for A, top right (transposed/rowwise) + K.fill_block(A, &mut map.A, 0, n, MatrixShape::T); + } + MatrixTriangle::Tril => { + K.fill_missing_diag(P, 0); // before adding P, since tril form + K.fill_block(P, &mut map.P, 0, 0, MatrixShape::T); + // fill in value for A, bottom left (not transposed) + K.fill_block(A, &mut map.A, n, 0, MatrixShape::N); + } + } + + // track the next sparse column to fill (assuming triu fill) + let mut pcol = m + n; //next sparse column to fill + let mut sparse_map_iter = map.sparse_maps.iter_mut(); + + for (i, cone) in cones.iter().enumerate() { + let row = cones.rng_cones[i].start + n; + + // add the Hs blocks in the lower right + let blockdim = cone.numel(); + let block = &mut map.Hsblocks[cones.rng_blocks[i].clone()]; + + if cone.Hs_is_diagonal() { + K.fill_diag(block, row, blockdim); + } else { + K.fill_dense_triangle(block, row, blockdim, shape); + } + + //add sparse expansions columns for sparse cones + if let Some(sc) = cone.to_sparse() { + let thismap = sparse_map_iter.next().unwrap(); + sc.csc_fill_sparsecone(thismap, K, row, pcol, shape); + pcol += thismap.pdim(); + } + } + + // backshift the colptrs to recover K.p again + K.backshift_colptrs(); + + // Now we can populate the index of the full diagonal. + // We have filled in structural zeros on it everywhere. + + match shape { + MatrixTriangle::Triu => { + // matrix is triu, so diagonal is last in each column + map.diag_full.copy_from_slice(&K.colptr[1..]); + map.diag_full.iter_mut().for_each(|x| *x -= 1); + // and the diagonal of just the upper left + map.diagP.copy_from_slice(&K.colptr[1..=n]); + map.diagP.iter_mut().for_each(|x| *x -= 1); + } + + MatrixTriangle::Tril => { + // matrix is tril, so diagonal is first in each column + map.diag_full + .copy_from_slice(&K.colptr[0..K.colptr.len() - 1]); + // and the diagonal of just the upper left + map.diagP.copy_from_slice(&K.colptr[0..n]); + } + } +} + +#[test] +fn test_kkt_assembly_upper_lower() { + let P = CscMatrix::from(&[ + [1., 2., 4.], // + [0., 3., 5.], // + [0., 0., 6.], // + ]); + let A = CscMatrix::from(&[ + [7., 0., 8.], // + [0., 9., 10.], // + [1., 2., 3.], + ]); + + let Ku_true_diag = CscMatrix::from(&[ + [1., 2., 4., 7., 0., 1.], // + [0., 3., 5., 0., 9., 2.], // + [0., 0., 6., 8., 10., 3.], // + [0., 0., 0., -1., 0., 0.], // + [0., 0., 0., 0., -1., 0.], // + [0., 0., 0., 0., 0., -1.], // + ]); + + let Kl_true_diag = CscMatrix::from(&[ + [1., 0., 0., 0., 0., 0.], // + [2., 3., 0., 0., 0., 0.], // + [4., 5., 6., 0., 0., 0.], // + [7., 0., 8., -1., 0., 0.], // + [0., 9., 10., 0., -1., 0.], // + [1., 2., 3., 0., 0., -1.], // + ]); + + let Ku_true_dense = CscMatrix::from(&[ + [1., 2., 4., 7., 0., 1.], // + [0., 3., 5., 0., 9., 2.], // + [0., 0., 6., 8., 10., 3.], // + [0., 0., 0., -1., -1., -1.], // + [0., 0., 0., 0., -1., -1.], // + [0., 0., 0., 0., 0., -1.], // + ]); + + let Kl_true_dense = CscMatrix::from(&[ + [1., 0., 0., 0., 0., 0.], // + [2., 3., 0., 0., 0., 0.], // + [4., 5., 6., 0., 0., 0.], // + [7., 0., 8., -1., 0., 0.], // + [0., 9., 10., -1., -1., 0.], // + [1., 2., 3., -1., -1., -1.], // + ]); + + // diagonal lower right block tests + // -------------------------------- + let K = SupportedConeT::NonnegativeConeT(3); + let cones = CompositeCone::new(&[K]); + + let (mut Ku, mapu) = assemble_kkt_matrix(&P, &A, &cones, MatrixTriangle::Triu); + for i in mapu.Hsblocks { + Ku.nzval[i] = -1.; + } + assert_eq!(Ku, Ku_true_diag); + + let (mut Kl, mapl) = assemble_kkt_matrix(&P, &A, &cones, MatrixTriangle::Tril); + for i in mapl.Hsblocks { + Kl.nzval[i] = -1.; + } + assert_eq!(Kl, Kl_true_diag); + + // dense lower right block tests + // -------------------------------- + let K = SupportedConeT::ExponentialConeT(); + let cones = CompositeCone::new(&[K]); + + let (mut Ku, mapu) = assemble_kkt_matrix(&P, &A, &cones, MatrixTriangle::Triu); + for i in mapu.Hsblocks { + Ku.nzval[i] = -1.; + } + assert_eq!(Ku, Ku_true_dense); + + let (mut Kl, mapl) = assemble_kkt_matrix(&P, &A, &cones, MatrixTriangle::Tril); + for i in mapl.Hsblocks { + Kl.nzval[i] = -1.; + } + assert_eq!(Kl, Kl_true_dense); +} diff --git a/src/solver/core/kktsolvers/direct/quasidef/mod.rs b/src/solver/core/kktsolvers/direct/quasidef/mod.rs index 5cef5e0d..917d809e 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/mod.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/mod.rs @@ -4,12 +4,12 @@ use crate::algebra::*; pub mod ldlsolvers; //flatten direct KKT module structure -mod datamap; +mod datamaps; mod directldlkktsolver; -mod utils; -pub use datamap::*; +mod kkt_assembly; +use datamaps::*; pub use directldlkktsolver::*; -pub use utils::*; +use kkt_assembly::*; pub trait DirectLDLSolver { fn update_values(&mut self, index: &[usize], values: &[T]); diff --git a/src/solver/core/kktsolvers/direct/quasidef/utils.rs b/src/solver/core/kktsolvers/direct/quasidef/utils.rs deleted file mode 100644 index 960f1212..00000000 --- a/src/solver/core/kktsolvers/direct/quasidef/utils.rs +++ /dev/null @@ -1,406 +0,0 @@ -#![allow(non_snake_case)] - -use super::datamap::*; -use crate::algebra::*; -use crate::solver::core::cones::CompositeCone; -use crate::solver::core::cones::*; -use num_traits::Zero; -use std::iter::zip; - -pub(crate) fn allocate_kkt_Hsblocks(cones: &CompositeCone) -> Vec -where - T: FloatT, - Z: Zero + Clone, -{ - let mut nnz = 0; - if let Some(rng_last) = cones.rng_blocks.last() { - nnz = rng_last.end; - } - vec![Z::zero(); nnz] -} - -pub fn assemble_kkt_matrix( - P: &CscMatrix, - A: &CscMatrix, - cones: &CompositeCone, - shape: MatrixTriangle, -) -> (CscMatrix, LDLDataMap) { - let (m, n) = (A.nrows(), P.nrows()); - - let n_socs = cones.type_count(SupportedConeTag::SecondOrderCone); - let p_socs = 2 * n_socs; - let n_genpows = cones.type_count(SupportedConeTag::GenPowerCone); - let p_genpows = 3 * n_genpows; - - let mut maps = LDLDataMap::new(P, A, cones); - - // entries actually on the diagonal of P - let nnz_diagP = P.count_diagonal_entries(); - - // total entries in the Hs blocks - let nnz_Hsblocks = maps.Hsblocks.len(); - - // entries in the dense columns u/v of the - // sparse SOC expansion terms. 2 is for - // counting elements in both columns - let nnz_SOC_vecs = 2 * maps.SOC_u.iter().fold(0, |acc, block| acc + block.len()); - - // entries in the dense columns p.q.r of the - // sparse generalized power expansion terms. - let nnz_GenPow_vecs = maps.GenPow_p.iter().fold(0, |acc, block| acc + block.len()) - + maps.GenPow_q.iter().fold(0, |acc, block| acc + block.len()) - + maps.GenPow_r.iter().fold(0, |acc, block| acc + block.len()); - - //entries in the sparse SOC diagonal extension block - let nnz_SOC_ext = maps.SOC_D.len(); - //entries in the sparse generalized power diagonal extension block - let nnz_GenPow_ext = maps.GenPow_D.len(); - - let nnzKKT = P.nnz() + // Number of elements in P - n - // Number of elements in diagonal top left block - nnz_diagP + // remove double count on the diagonal if P has entries - A.nnz() + // Number of nonzeros in A - nnz_Hsblocks + // Number of elements in diagonal below A' - nnz_SOC_vecs + // Number of elements in sparse SOC off diagonal columns - nnz_SOC_ext + // Number of elements in diagonal of SOC extension - nnz_GenPow_vecs + // Number of elements in sparse generalized power off diagonal columns - nnz_GenPow_ext; // Number of elements in diagonal of generalized power extension - - let Kdim = m + n + p_socs + p_genpows; - let mut K = CscMatrix::::spalloc((Kdim, Kdim), nnzKKT); - - _kkt_assemble_colcounts(&mut K, P, A, cones, (m, n, p_socs, p_genpows), shape); - _kkt_assemble_fill( - &mut K, - &mut maps, - P, - A, - cones, - (m, n, p_socs, p_genpows), - shape, - ); - - (K, maps) -} - -fn _kkt_assemble_colcounts( - K: &mut CscMatrix, - P: &CscMatrix, - A: &CscMatrix, - cones: &CompositeCone, - mnp: (usize, usize, usize, usize), - shape: MatrixTriangle, -) { - let (m, n, p_socs, p_genpows) = (mnp.0, mnp.1, mnp.2, mnp.3); - - // use K.p to hold nnz entries in each - // column of the KKT matrix - K.colptr.fill(0); - - match shape { - MatrixTriangle::Triu => { - K.colcount_block(P, 0, MatrixShape::N); - K.colcount_missing_diag(P, 0); - K.colcount_block(A, n, MatrixShape::T); - } - MatrixTriangle::Tril => { - K.colcount_missing_diag(P, 0); - K.colcount_block(P, 0, MatrixShape::T); - K.colcount_block(A, 0, MatrixShape::N); - } - } - - // add the Hs blocks in the lower right - for (i, cone) in cones.iter().enumerate() { - let firstcol = cones.rng_cones[i].start + n; - let blockdim = cone.numel(); - if cone.Hs_is_diagonal() { - K.colcount_diag(firstcol, blockdim); - } else { - K.colcount_dense_triangle(firstcol, blockdim, shape); - } - } - - // count dense columns for each SOC - let mut socidx = 0; // which SOC are we working on? - - for (i, cone) in cones.iter().enumerate() { - if let SupportedCone::SecondOrderCone(socone) = cone { - // we will add the u and v columns for this cone - let nvars = socone.numel(); - let headidx = cones.rng_cones[i].start; - - // which column does u go into? - let col = m + n + 2 * socidx; - - match shape { - MatrixTriangle::Triu => { - K.colcount_colvec(nvars, headidx + n, col); // u column - K.colcount_colvec(nvars, headidx + n, col + 1); // v column - } - MatrixTriangle::Tril => { - K.colcount_rowvec(nvars, col, headidx + n); // u row - K.colcount_rowvec(nvars, col + 1, headidx + n); // v row - } - } - socidx += 1; - } - } - - // add diagonal block in the lower RH corner - // to allow for the diagonal terms in SOC expansion - K.colcount_diag(n + m, p_socs); - - // count dense columns for each generalized power cone - let mut genpowidx = 0; // which Genpow are we working on? - - for (i, cone) in cones.iter().enumerate() { - if let SupportedCone::GenPowerCone(gpcone) = cone { - // we will add the p,q,r columns for this cone - let nvars = gpcone.numel(); - let dim1 = gpcone.dim1(); - let dim2 = gpcone.dim2(); - let headidx = cones.rng_cones[i].start; - - // which column does q go into? - let col = m + n + 2 * socidx + 3 * genpowidx; - - match shape { - MatrixTriangle::Triu => { - K.colcount_colvec(dim1, headidx + n, col); // q column - K.colcount_colvec(dim2, headidx + n + dim1, col + 1); // r column - K.colcount_colvec(nvars, headidx + n, col + 2); // p column - } - MatrixTriangle::Tril => { - K.colcount_rowvec(dim1, col, headidx + n); // q row - K.colcount_rowvec(dim2, col + 1, headidx + n + dim1); // r row - K.colcount_rowvec(nvars, col + 2, headidx + n); // p row - } - } - genpowidx += 1; - } - } - - // add diagonal block in the lower RH corner - // to allow for the diagonal terms in generalized power expansion - K.colcount_diag(n + m + p_socs, p_genpows); -} - -fn _kkt_assemble_fill( - K: &mut CscMatrix, - maps: &mut LDLDataMap, - P: &CscMatrix, - A: &CscMatrix, - cones: &CompositeCone, - mnp: (usize, usize, usize, usize), - shape: MatrixTriangle, -) { - let (m, n, p_socs, p_genpows) = (mnp.0, mnp.1, mnp.2, mnp.3); - - // cumsum total entries to convert to K.p - K.colcount_to_colptr(); - - match shape { - MatrixTriangle::Triu => { - K.fill_block(P, &mut maps.P, 0, 0, MatrixShape::N); - K.fill_missing_diag(P, 0); // after adding P, since triu form - // fill in value for A, top right (transposed/rowwise) - K.fill_block(A, &mut maps.A, 0, n, MatrixShape::T); - } - MatrixTriangle::Tril => { - K.fill_missing_diag(P, 0); // before adding P, since tril form - K.fill_block(P, &mut maps.P, 0, 0, MatrixShape::T); - // fill in value for A, bottom left (not transposed) - K.fill_block(A, &mut maps.A, n, 0, MatrixShape::N); - } - } - - // add the the Hs blocks in the lower right - for (i, (cone, rng_cone)) in zip(cones.iter(), &cones.rng_cones).enumerate() { - let firstcol = rng_cone.start + n; - let blockdim = cone.numel(); - let block = &mut maps.Hsblocks[cones.rng_blocks[i].clone()]; - if cone.Hs_is_diagonal() { - K.fill_diag(block, firstcol, blockdim); - } else { - K.fill_dense_triangle(block, firstcol, blockdim, shape); - } - } - - // fill in dense columns for each SOC - let mut socidx = 0; //which SOC are we working on? - - for (i, cone) in cones.iter().enumerate() { - if let SupportedCone::SecondOrderCone(_) = cone { - let headidx = cones.rng_cones[i].start; - - // which column does u go into (if triu)? - let col = m + n + 2 * socidx; - - // fill structural zeros for u and v columns for this cone - // note v is the first extra row/column, u is second - match shape { - MatrixTriangle::Triu => { - K.fill_colvec(&mut maps.SOC_v[socidx], headidx + n, col); //u - K.fill_colvec(&mut maps.SOC_u[socidx], headidx + n, col + 1); - //v - } - MatrixTriangle::Tril => { - K.fill_rowvec(&mut maps.SOC_v[socidx], col, headidx + n); //u - K.fill_rowvec(&mut maps.SOC_u[socidx], col + 1, headidx + n); - //v - } - } - - socidx += 1; - } - } - - // fill in SOC diagonal extension with diagonal of structural zeros - K.fill_diag(&mut maps.SOC_D, n + m, p_socs); - - // fill in dense columns for each generalized power cone - let mut genpowidx = 0; //which generalized power cone are we working on? - - for (i, cone) in cones.iter().enumerate() { - if let SupportedCone::GenPowerCone(gpcone) = cone { - let headidx = cones.rng_cones[i].start; - let dim1 = gpcone.dim1(); - - // which column does q go into (if triu)? - let col = m + n + 2 * socidx + 3 * genpowidx; - - // fill structural zeros for p,q,r columns for this cone - match shape { - MatrixTriangle::Triu => { - K.fill_colvec(&mut maps.GenPow_q[genpowidx], headidx + n, col); //q - K.fill_colvec(&mut maps.GenPow_r[genpowidx], headidx + n + dim1, col + 1); //r - K.fill_colvec(&mut maps.GenPow_p[genpowidx], headidx + n, col + 2); - //p - //v - } - MatrixTriangle::Tril => { - K.fill_rowvec(&mut maps.GenPow_q[genpowidx], col, headidx + n); //q - K.fill_rowvec(&mut maps.GenPow_r[genpowidx], col + 1, headidx + n + dim1); //r - K.fill_rowvec(&mut maps.GenPow_p[genpowidx], col + 2, headidx + n); - //p - //v - } - } - - genpowidx += 1; - } - } - - // fill in SOC diagonal extension with diagonal of structural zeros - K.fill_diag(&mut maps.GenPow_D, n + m + p_socs, p_genpows); - - // backshift the colptrs to recover K.p again - K.backshift_colptrs(); - - // Now we can populate the index of the full diagonal. - // We have filled in structural zeros on it everywhere. - - match shape { - MatrixTriangle::Triu => { - // matrix is triu, so diagonal is last in each column - maps.diag_full.copy_from_slice(&K.colptr[1..]); - maps.diag_full.iter_mut().for_each(|x| *x -= 1); - // and the diagonal of just the upper left - maps.diagP.copy_from_slice(&K.colptr[1..=n]); - maps.diagP.iter_mut().for_each(|x| *x -= 1); - } - - MatrixTriangle::Tril => { - // matrix is tril, so diagonal is first in each column - maps.diag_full - .copy_from_slice(&K.colptr[0..K.colptr.len() - 1]); - // and the diagonal of just the upper left - maps.diagP.copy_from_slice(&K.colptr[0..n]); - } - } -} - -#[test] -fn test_kkt_assembly_upper_lower() { - let P = CscMatrix::from(&[ - [1., 2., 4.], // - [0., 3., 5.], // - [0., 0., 6.], // - ]); - let A = CscMatrix::from(&[ - [7., 0., 8.], // - [0., 9., 10.], // - [1., 2., 3.], - ]); - - let Ku_true_diag = CscMatrix::from(&[ - [1., 2., 4., 7., 0., 1.], // - [0., 3., 5., 0., 9., 2.], // - [0., 0., 6., 8., 10., 3.], // - [0., 0., 0., -1., 0., 0.], // - [0., 0., 0., 0., -1., 0.], // - [0., 0., 0., 0., 0., -1.], // - ]); - - let Kl_true_diag = CscMatrix::from(&[ - [1., 0., 0., 0., 0., 0.], // - [2., 3., 0., 0., 0., 0.], // - [4., 5., 6., 0., 0., 0.], // - [7., 0., 8., -1., 0., 0.], // - [0., 9., 10., 0., -1., 0.], // - [1., 2., 3., 0., 0., -1.], // - ]); - - let Ku_true_dense = CscMatrix::from(&[ - [1., 2., 4., 7., 0., 1.], // - [0., 3., 5., 0., 9., 2.], // - [0., 0., 6., 8., 10., 3.], // - [0., 0., 0., -1., -1., -1.], // - [0., 0., 0., 0., -1., -1.], // - [0., 0., 0., 0., 0., -1.], // - ]); - - let Kl_true_dense = CscMatrix::from(&[ - [1., 0., 0., 0., 0., 0.], // - [2., 3., 0., 0., 0., 0.], // - [4., 5., 6., 0., 0., 0.], // - [7., 0., 8., -1., 0., 0.], // - [0., 9., 10., -1., -1., 0.], // - [1., 2., 3., -1., -1., -1.], // - ]); - - // diagonal lower right block tests - // -------------------------------- - let K = SupportedConeT::NonnegativeConeT(3); - let cones = CompositeCone::new(&[K]); - - let (mut Ku, mapu) = assemble_kkt_matrix(&P, &A, &cones, MatrixTriangle::Triu); - for i in mapu.Hsblocks { - Ku.nzval[i] = -1.; - } - assert_eq!(Ku, Ku_true_diag); - - let (mut Kl, mapl) = assemble_kkt_matrix(&P, &A, &cones, MatrixTriangle::Tril); - for i in mapl.Hsblocks { - Kl.nzval[i] = -1.; - } - assert_eq!(Kl, Kl_true_diag); - - // dense lower right block tests - // -------------------------------- - let K = SupportedConeT::ExponentialConeT(); - let cones = CompositeCone::new(&[K]); - - let (mut Ku, mapu) = assemble_kkt_matrix(&P, &A, &cones, MatrixTriangle::Triu); - for i in mapu.Hsblocks { - Ku.nzval[i] = -1.; - } - assert_eq!(Ku, Ku_true_dense); - - let (mut Kl, mapl) = assemble_kkt_matrix(&P, &A, &cones, MatrixTriangle::Tril); - for i in mapl.Hsblocks { - Kl.nzval[i] = -1.; - } - assert_eq!(Kl, Kl_true_dense); -} diff --git a/src/solver/implementations/default/info_print.rs b/src/solver/implementations/default/info_print.rs index ce0fd4c4..5cebcc00 100644 --- a/src/solver/implementations/default/info_print.rs +++ b/src/solver/implementations/default/info_print.rs @@ -210,14 +210,15 @@ fn _get_precision_string() -> String { fn _print_conedims_by_type(cones: &CompositeCone, conetag: SupportedConeTag) { let maxlistlen = 5; + let count = cones.get_type_count(conetag); + //skip if there are none of this type - if !cones.type_counts.contains_key(&conetag) { + if count == 0 { return; } // how many of this type of cone? let name = conetag.as_str(); - let count = cones.type_counts[&conetag]; // drops trailing "Cone" part of name let name = &name[0..name.len() - 4]; diff --git a/src/solver/implementations/default/kktsystem.rs b/src/solver/implementations/default/kktsystem.rs index 1c5b4032..8cdc4b8e 100644 --- a/src/solver/implementations/default/kktsystem.rs +++ b/src/solver/implementations/default/kktsystem.rs @@ -232,8 +232,7 @@ where .solve(None, Some(&mut variables.z), settings.core()); } else { //QP initialization - self.workx.copy_from(&data.q); - self.workx.negate(); + self.workx.scalarop_from(|q| -q, &data.q); self.workz.copy_from(&data.b); self.kktsolver.setrhs(&self.workx, &self.workz); is_success = self.kktsolver.solve( @@ -241,8 +240,7 @@ where Some(&mut variables.z), settings.core(), ); - variables.s.copy_from(&variables.z); - variables.s.negate(); + variables.s.scalarop_from(|z| -z, &variables.z); } is_success } diff --git a/tests/basic_sdp.rs b/tests/basic_sdp.rs index 7c6766af..815ce3ab 100644 --- a/tests/basic_sdp.rs +++ b/tests/basic_sdp.rs @@ -90,7 +90,7 @@ fn test_sdp_primal_infeasible() { A2.negate(); let A = CscMatrix::vcat(&A, &A2); b.extend(vec![0.0; b.len()]); - cones.extend([cones[0]]); + cones.extend([cones[0].clone()]); let settings = DefaultSettings::default(); From 181b1ce159801724f8e3ccd753b5db98ac88316a Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Fri, 8 Sep 2023 12:56:53 +0100 Subject: [PATCH 30/52] rustification --- src/solver/core/cones/genpowcone.rs | 22 +++++++++---------- .../kktsolvers/direct/quasidef/datamaps.rs | 10 ++++----- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs index 0528371d..c2f93a0a 100644 --- a/src/solver/core/cones/genpowcone.rs +++ b/src/solver/core/cones/genpowcone.rs @@ -276,12 +276,11 @@ where let two: T = (2f64).as_T(); let dim1 = self.dim1(); - if s[..dim1].iter().all(|x| *x > T::zero()) { - let mut res = T::zero(); - for i in 0..dim1 { - res += two * α[i] * s[i].logsafe() - } - res = T::exp(res) - s[dim1..].sumsq(); + if s[..dim1].iter().all(|&x| x > T::zero()) { + let res = zip(α, &s[..dim1]).fold(T::zero(), |res, (&αi, &si)| -> T { + res + two * αi * si.logsafe() + }); + let res = T::exp(res) - s[dim1..].sumsq(); if res > T::zero() { return true; @@ -299,12 +298,11 @@ where let two: T = (2.).as_T(); let dim1 = self.dim1(); - if z[..dim1].iter().all(|x| *x > T::zero()) { - let mut res = T::zero(); - for i in 0..dim1 { - res += two * α[i] * (z[i] / α[i]).logsafe() - } - res = T::exp(res) - z[dim1..].sumsq(); + if z[..dim1].iter().all(|&x| x > T::zero()) { + let res = zip(α, &z[..dim1]).fold(T::zero(), |res, (&αi, &zi)| -> T { + res + two * αi * (zi / αi).logsafe() + }); + let res = T::exp(res) - z[dim1..].sumsq(); if res > T::zero() { return true; diff --git a/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs b/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs index 961db4d5..b01fa864 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs @@ -145,7 +145,7 @@ where T: FloatT, { fn expansion_map(&self) -> SparseExpansionMap { - SparseExpansionMap::SOCExpansionMap(SOCExpansionMap::new(&self)) + SparseExpansionMap::SOCExpansionMap(SOCExpansionMap::new(self)) } fn csc_colcount_sparsecone( @@ -156,7 +156,7 @@ where col: usize, shape: MatrixTriangle, ) { - let map = self.recover_map(&map); + let map = self.recover_map(map); let nvars = self.numel(); match shape { @@ -260,7 +260,7 @@ where T: FloatT, { fn expansion_map(&self) -> SparseExpansionMap { - SparseExpansionMap::GenPowExpansionMap(GenPowExpansionMap::new(&self)) + SparseExpansionMap::GenPowExpansionMap(GenPowExpansionMap::new(self)) } fn csc_colcount_sparsecone( @@ -271,7 +271,7 @@ where col: usize, shape: MatrixTriangle, ) { - let map = self.recover_map(&map); + let map = self.recover_map(map); let nvars = self.numel(); let dim1 = self.dim1(); let dim2 = self.dim2(); @@ -325,7 +325,7 @@ where updateFcn: UpdateFcn, scaleFcn: ScaleFcn, ) { - let map = self.recover_map(&map); + let map = self.recover_map(map); let data = &self.data; let sqrtμ = data.μ.sqrt(); From a2802ab730734d03bda90e0fe5fef9f89cfbc908 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Fri, 8 Sep 2023 13:03:40 +0100 Subject: [PATCH 31/52] rustification --- src/algebra/dense/core.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/algebra/dense/core.rs b/src/algebra/dense/core.rs index 6306d872..bc753f48 100644 --- a/src/algebra/dense/core.rs +++ b/src/algebra/dense/core.rs @@ -42,6 +42,7 @@ pub struct Matrix { /// [0.0, 4.0]]); // ``` // +#[allow(clippy::needless_range_loop)] impl<'a, I, J, T> From for Matrix where I: IntoIterator, @@ -51,7 +52,7 @@ where fn from(rows: I) -> Matrix { let rows: Vec> = rows .into_iter() - .map(|r| r.into_iter().map(|&v| v).collect()) + .map(|r| r.into_iter().copied().collect()) .collect(); let m = rows.len(); From d53ca91047c59d12ff4bdfc3a2cff77fb24ae933 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Sun, 10 Sep 2023 08:58:03 +0100 Subject: [PATCH 32/52] fix identity init for SOCs --- src/solver/core/cones/socone.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/solver/core/cones/socone.rs b/src/solver/core/cones/socone.rs index fbe4617c..2c1ba7f9 100644 --- a/src/solver/core/cones/socone.rs +++ b/src/solver/core/cones/socone.rs @@ -82,11 +82,14 @@ where } fn set_identity_scaling(&mut self) { - self.d = T::one(); + self.w.fill(T::zero()); + self.w[0] = T::one(); + + self.d = (0.5).as_T(); self.u.fill(T::zero()); + self.u[0] = T::FRAC_1_SQRT_2(); self.v.fill(T::zero()); self.η = T::one(); - self.w.fill(T::zero()); } fn update_scaling( From da1e8a30dec8a15d258ce5cc4265d7f62611406e Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Wed, 13 Sep 2023 16:48:18 +0100 Subject: [PATCH 33/52] julia impl sync --- src/solver/core/cones/expcone.rs | 1 - src/solver/core/cones/genpowcone.rs | 2 +- src/solver/core/cones/nonsymmetric_common.rs | 4 ++-- src/solver/core/cones/powcone.rs | 1 - src/solver/implementations/default/kktsystem.rs | 11 +++++++---- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/solver/core/cones/expcone.rs b/src/solver/core/cones/expcone.rs index 619e88e4..b8b8c723 100644 --- a/src/solver/core/cones/expcone.rs +++ b/src/solver/core/cones/expcone.rs @@ -150,7 +150,6 @@ where ) -> (T, T) { let step = settings.linesearch_backtrack_step; let αmin = settings.min_terminate_step_length; - let mut work = [T::zero(); 3]; let _is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs index c2f93a0a..20831da3 100644 --- a/src/solver/core/cones/genpowcone.rs +++ b/src/solver/core/cones/genpowcone.rs @@ -66,7 +66,7 @@ where pub struct GenPowerCone { pub α: Vec, // power defining the cone. length determines dim1 - pub dim2: usize, // dimension of w + dim2: usize, // dimension of w pub data: Box>, // Boxed so that the enum_dispatch variant isn't huge } diff --git a/src/solver/core/cones/nonsymmetric_common.rs b/src/solver/core/cones/nonsymmetric_common.rs index 1e27be35..168e65c6 100644 --- a/src/solver/core/cones/nonsymmetric_common.rs +++ b/src/solver/core/cones/nonsymmetric_common.rs @@ -167,7 +167,7 @@ pub(crate) fn backtrack_search( q: &[T], α_init: T, α_min: T, - backtrack: T, + step: T, is_in_cone_fcn: impl Fn(&[T]) -> bool, work: &mut [T], ) -> T @@ -183,7 +183,7 @@ where if is_in_cone_fcn(work) { break; } - α *= backtrack; + α *= step; if α < α_min { α = T::zero(); break; diff --git a/src/solver/core/cones/powcone.rs b/src/solver/core/cones/powcone.rs index 4b74b4e2..99085cd7 100644 --- a/src/solver/core/cones/powcone.rs +++ b/src/solver/core/cones/powcone.rs @@ -153,7 +153,6 @@ where ) -> (T, T) { let step = settings.linesearch_backtrack_step; let αmin = settings.min_terminate_step_length; - let mut work = [T::zero(); 3]; let _is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; diff --git a/src/solver/implementations/default/kktsystem.rs b/src/solver/implementations/default/kktsystem.rs index 8cdc4b8e..e9d0a24e 100644 --- a/src/solver/implementations/default/kktsystem.rs +++ b/src/solver/implementations/default/kktsystem.rs @@ -204,24 +204,27 @@ where data: &DefaultProblemData, settings: &DefaultSettings, ) -> bool { - let is_success; + let mut is_success; if data.P.nnz() == 0 { // LP initialization - // solve with [0;b] as a RHS to get (x,-s) initializers // zero out any sparse cone variables at end self.workx.fill(T::zero()); self.workz.copy_from(&data.b); self.kktsolver.setrhs(&self.workx, &self.workz); - self.kktsolver.solve( + is_success = self.kktsolver.solve( Some(&mut variables.x), Some(&mut variables.s), settings.core(), ); variables.s.negate(); - // solve with [-c;0] as a RHS to get z initializer + if !is_success { + return is_success; + } + + // solve with [-q;0] as a RHS to get z initializer // zero out any sparse cone variables at end self.workx.axpby(-T::one(), &data.q, T::zero()); self.workz.fill(T::zero()); From 19913226ce8908815e4f80cf0bfa73122577aa08 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Wed, 13 Sep 2023 16:55:15 +0100 Subject: [PATCH 34/52] julia sync --- src/solver/core/cones/genpowcone.rs | 11 +++-------- src/solver/core/solver.rs | 4 ++-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs index 20831da3..992855a9 100644 --- a/src/solver/core/cones/genpowcone.rs +++ b/src/solver/core/cones/genpowcone.rs @@ -244,25 +244,20 @@ where fn compute_barrier(&mut self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { let mut barrier = T::zero(); - let mut work = std::mem::take(&mut self.data.work); - work.waxpby(T::one(), z, α, dz); - barrier += self.barrier_dual(&work); - work.waxpby(T::one(), s, α, ds); barrier += self.barrier_primal(&work); + work.waxpby(T::one(), z, α, dz); + barrier += self.barrier_dual(&work); + self.data.work = work; barrier } } -//------------------------------------- -// Dual scaling -//------------------------------------- - impl NonsymmetricCone for GenPowerCone where T: FloatT, diff --git a/src/solver/core/solver.rs b/src/solver/core/solver.rs index 236b1d63..bd222e40 100644 --- a/src/solver/core/solver.rs +++ b/src/solver/core/solver.rs @@ -476,7 +476,7 @@ mod internal { } fn backtrack_step_to_barrier(&mut self, αinit: T) -> T { - let backtrack = self.settings.core().linesearch_backtrack_step; + let step = self.settings.core().linesearch_backtrack_step; let mut α = αinit; for _ in 0..50 { @@ -484,7 +484,7 @@ mod internal { if barrier < T::one() { return α; } else { - α = backtrack * α; // backtrack line search + α = step * α; } } α From 7751c9b41c0a81c523dd7e14593e5c0d1db099c5 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Thu, 14 Sep 2023 11:24:24 +0100 Subject: [PATCH 35/52] port dense SOC cones --- src/solver/core/cones/compositecone.rs | 5 + src/solver/core/cones/expcone.rs | 4 + src/solver/core/cones/genpowcone.rs | 6 + src/solver/core/cones/mod.rs | 3 + src/solver/core/cones/nonnegativecone.rs | 4 + src/solver/core/cones/powcone.rs | 4 + src/solver/core/cones/psdtrianglecone.rs | 4 + src/solver/core/cones/socone.rs | 160 ++++++++++++------ src/solver/core/cones/zerocone.rs | 4 + .../kktsolvers/direct/quasidef/datamaps.rs | 17 +- .../direct/quasidef/directldlkktsolver.rs | 3 +- .../direct/quasidef/kkt_assembly.rs | 6 +- 12 files changed, 159 insertions(+), 61 deletions(-) diff --git a/src/solver/core/cones/compositecone.rs b/src/solver/core/cones/compositecone.rs index ab5e4e3a..54403d8f 100644 --- a/src/solver/core/cones/compositecone.rs +++ b/src/solver/core/cones/compositecone.rs @@ -182,6 +182,11 @@ where self._is_symmetric } + fn is_sparse_expandable(&self) -> bool { + //This will probably never be called + self.cones.iter().all(|cone| cone.is_sparse_expandable()) + } + fn allows_primal_dual_scaling(&self) -> bool { self.cones .iter() diff --git a/src/solver/core/cones/expcone.rs b/src/solver/core/cones/expcone.rs index b8b8c723..b8227bdf 100644 --- a/src/solver/core/cones/expcone.rs +++ b/src/solver/core/cones/expcone.rs @@ -53,6 +53,10 @@ where false } + fn is_sparse_expandable(&self) -> bool { + false + } + fn allows_primal_dual_scaling(&self) -> bool { true } diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs index 992855a9..97070983 100644 --- a/src/solver/core/cones/genpowcone.rs +++ b/src/solver/core/cones/genpowcone.rs @@ -106,6 +106,12 @@ where false } + fn is_sparse_expandable(&self) -> bool { + // we do not curently have a way of representing + // this cone in non-expanded form + true + } + fn allows_primal_dual_scaling(&self) -> bool { false } diff --git a/src/solver/core/cones/mod.rs b/src/solver/core/cones/mod.rs index f8219561..12f3789a 100644 --- a/src/solver/core/cones/mod.rs +++ b/src/solver/core/cones/mod.rs @@ -48,6 +48,9 @@ where fn degree(&self) -> usize; fn numel(&self) -> usize; + //Can the cone provide a sparse expanded representation? + fn is_sparse_expandable(&self) -> bool; + // is the cone symmetric? NB: zero cone still reports true fn is_symmetric(&self) -> bool; diff --git a/src/solver/core/cones/nonnegativecone.rs b/src/solver/core/cones/nonnegativecone.rs index bd808658..214fc45e 100644 --- a/src/solver/core/cones/nonnegativecone.rs +++ b/src/solver/core/cones/nonnegativecone.rs @@ -45,6 +45,10 @@ where true } + fn is_sparse_expandable(&self) -> bool { + false + } + fn allows_primal_dual_scaling(&self) -> bool { true } diff --git a/src/solver/core/cones/powcone.rs b/src/solver/core/cones/powcone.rs index 99085cd7..6a5fee1b 100644 --- a/src/solver/core/cones/powcone.rs +++ b/src/solver/core/cones/powcone.rs @@ -55,6 +55,10 @@ where false } + fn is_sparse_expandable(&self) -> bool { + false + } + fn allows_primal_dual_scaling(&self) -> bool { true } diff --git a/src/solver/core/cones/psdtrianglecone.rs b/src/solver/core/cones/psdtrianglecone.rs index 26e490f9..4efb5402 100644 --- a/src/solver/core/cones/psdtrianglecone.rs +++ b/src/solver/core/cones/psdtrianglecone.rs @@ -94,6 +94,10 @@ where true } + fn is_sparse_expandable(&self) -> bool { + false + } + fn allows_primal_dual_scaling(&self) -> bool { true } diff --git a/src/solver/core/cones/socone.rs b/src/solver/core/cones/socone.rs index 2c1c6f6b..c8de4008 100644 --- a/src/solver/core/cones/socone.rs +++ b/src/solver/core/cones/socone.rs @@ -8,18 +8,36 @@ use crate::{ // Second order Cone // ------------------------------------- -pub struct SecondOrderCone { - dim: usize, - //internal working variables for W and its products - w: Vec, - //scaled version of (s,z) - λ: Vec, +pub struct SecondOrderConeSparseData { //vectors for rank 2 update representation of W^2 pub u: Vec, pub v: Vec, + //additional scalar terms for rank-2 rep - d: T, + pub d: T, +} + +impl SecondOrderConeSparseData +where + T: FloatT, +{ + pub fn new(dim: usize) -> Self { + Self { + u: vec![T::zero(); dim], + v: vec![T::zero(); dim], + d: T::one(), + } + } +} + +pub struct SecondOrderCone { + pub dim: usize, + //internal working variables for W and its products + pub w: Vec, + //scaled version of (s,z) + pub λ: Vec, pub η: T, + pub sparse_data: Option>, } impl SecondOrderCone @@ -27,15 +45,28 @@ where T: FloatT, { pub fn new(dim: usize) -> Self { + const SOC_NO_EXPANSION_MAX_SIZE: usize = 4; + assert!(dim >= 2); + + let w = vec![T::zero(); dim]; + let λ = vec![T::zero(); dim]; + let η = T::zero(); + + let sparse_data = { + if dim > SOC_NO_EXPANSION_MAX_SIZE { + Some(SecondOrderConeSparseData::new(dim)) + } else { + None + } + }; + Self { dim, - w: vec![T::zero(); dim], - λ: vec![T::zero(); dim], - u: vec![T::zero(); dim], - v: vec![T::zero(); dim], - d: T::one(), - η: T::zero(), + w, + λ, + η, + sparse_data, } } } @@ -57,6 +88,10 @@ where true } + fn is_sparse_expandable(&self) -> bool { + self.sparse_data.is_some() + } + fn allows_primal_dual_scaling(&self) -> bool { true } @@ -88,12 +123,14 @@ where fn set_identity_scaling(&mut self) { self.w.fill(T::zero()); self.w[0] = T::one(); - - self.d = (0.5).as_T(); - self.u.fill(T::zero()); - self.u[0] = T::FRAC_1_SQRT_2(); - self.v.fill(T::zero()); self.η = T::one(); + + if let Some(sparse_data) = &mut self.sparse_data { + sparse_data.d = (0.5).as_T(); + sparse_data.u.fill(T::zero()); + sparse_data.u[0] = T::FRAC_1_SQRT_2(); + sparse_data.v.fill(T::zero()); + } } fn update_scaling( @@ -115,14 +152,17 @@ where return false; } + //the leading scalar term for W^TW + self.η = T::sqrt(sscale / zscale); + // construct w and normalize let w = &mut self.w; w.copy_from(s); w.scale(sscale.recip()); w[0] += z[0] / zscale; w[1..].axpby(-zscale.recip(), &z[1..], T::one()); - let wscale = _sqrt_soc_residual(w); + let wscale = _sqrt_soc_residual(w); // Fail if w is not an interior point if wscale.is_zero() { return false; @@ -132,48 +172,68 @@ where // try to force badly scaled w to come out normalized let w1sq = w[1..].sumsq(); w[0] = T::sqrt(T::one() + w1sq); - let wsq = w[0] * w[0] + w1sq; - - //various intermediate calcs for u,v,d,η - let α = two * w[0]; - - //Scalar d is the upper LH corner of the diagonal - //term in the rank-2 update form of W^TW - let wsqinv = wsq.recip(); - self.d = half * wsqinv; - - //the leading scalar term for W^TW - self.η = T::sqrt(sscale / zscale); - - //the vectors for the rank two update - //representation of W^TW - let u0 = T::sqrt(wsq - self.d); - let u1 = α / u0; - let v0 = T::zero(); - let v1 = T::sqrt(two * (two + wsqinv) / (two * wsq - wsqinv)); - - self.u[0] = u0; - self.u[1..].axpby(u1, &self.w[1..], T::zero()); - self.v[0] = v0; - self.v[1..].axpby(v1, &self.w[1..], T::zero()); //λ = Wz. Use inner function here because can't //borrow self and self.λ at the same time - _soc_mul_W_inner(&mut self.λ, z, T::one(), T::zero(), &self.w, self.η); + _soc_mul_W_inner(&mut self.λ, z, T::one(), T::zero(), w, self.η); + + if let Some(sparse_data) = &mut self.sparse_data { + //various intermediate calcs for u,v,d,η + let α = two * w[0]; + + //Scalar d is the upper LH corner of the diagonal + //term in the rank-2 update form of W^TW + let wsq = w[0] * w[0] + w1sq; + let wsqinv = wsq.recip(); + sparse_data.d = half * wsqinv; + + //the vectors for the rank two update + //representation of W^TW + let u0 = T::sqrt(wsq - sparse_data.d); + let u1 = α / u0; + let v0 = T::zero(); + let v1 = T::sqrt(two * (two + wsqinv) / (two * wsq - wsqinv)); + + sparse_data.u[0] = u0; + sparse_data.u[1..].axpby(u1, &self.w[1..], T::zero()); + sparse_data.v[0] = v0; + sparse_data.v[1..].axpby(v1, &self.w[1..], T::zero()); + } true } fn Hs_is_diagonal(&self) -> bool { - true + self.is_sparse_expandable() } fn get_Hs(&self, Hsblock: &mut [T]) { - //NB: we are returning here the diagonal D block from the - //sparse representation of W^TW, but not the - //extra two entries at the bottom right of the block. - Hsblock.fill(self.η * self.η); - Hsblock[0] *= self.d; + if let Some(sparse_data) = &self.sparse_data { + // For sparse form, we are returning here the diagonal D block + // from the sparse representation of W^TW, but not the + // extra two entries at the bottom right of the block. + // The ConicVector for s and z (and its views) don't + // know anything about the 2 extra sparsifying entries + Hsblock.fill(self.η * self.η); + Hsblock[0] *= sparse_data.d; + } else { + let two: T = (2.).as_T(); + // for dense form, we return H = \eta^2 (2*ww^T - J), where + // J = diag(1,-I). We are packing into dense triu form + Hsblock[0] = two * self.w[0] * self.w[0] - T::one(); + let mut hidx = 1; + + for col in 1..self.dim { + let wcol = self.w[col]; + for row in 0..=col { + Hsblock[hidx] = two * self.w[row] * wcol; + hidx += 1 + } + //go back to add the offset term from J + Hsblock[hidx - 1] += T::one() + } + Hsblock.scale(self.η * self.η); + } } fn mul_Hs(&mut self, y: &mut [T], x: &[T], work: &mut [T]) { diff --git a/src/solver/core/cones/zerocone.rs b/src/solver/core/cones/zerocone.rs index 9f4cca01..271f1513 100644 --- a/src/solver/core/cones/zerocone.rs +++ b/src/solver/core/cones/zerocone.rs @@ -42,6 +42,10 @@ where true } + fn is_sparse_expandable(&self) -> bool { + false + } + fn allows_primal_dual_scaling(&self) -> bool { true } diff --git a/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs b/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs index b01fa864..bb9f6845 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs @@ -23,16 +23,13 @@ impl<'a, T> SupportedCone where T: FloatT, { - pub(crate) fn to_sparse(&'a self) -> Option> { + pub(crate) fn to_sparse_expansion(&'a self) -> Option> { match self { SupportedCone::SecondOrderCone(sc) => Some(SparseExpansionCone::SecondOrderCone(sc)), SupportedCone::GenPowerCone(sc) => Some(SparseExpansionCone::GenPowerCone(sc)), _ => None, } } - fn is_sparsifiable(&self) -> bool { - self.to_sparse().is_some() - } } #[enum_dispatch] @@ -56,6 +53,7 @@ impl SparseExpansionMapTrait for Vec { type UpdateFcn = fn(&mut BoxedDirectLDLSolver, &mut CscMatrix, &[usize], &[T]) -> (); type ScaleFcn = fn(&mut BoxedDirectLDLSolver, &mut CscMatrix, &[usize], T) -> (); + #[enum_dispatch] pub(crate) trait SparseExpansionConeTrait where @@ -206,12 +204,14 @@ where updateFcn: UpdateFcn, scaleFcn: ScaleFcn, ) { + let sparse_data = self.sparse_data.as_ref().unwrap(); + let map = self.recover_map(map); let η2 = self.η * self.η; // off diagonal columns (or rows) - updateFcn(ldl, K, &map.u, &self.u); - updateFcn(ldl, K, &map.v, &self.v); + updateFcn(ldl, K, &map.u, &sparse_data.u); + updateFcn(ldl, K, &map.v, &sparse_data.v); scaleFcn(ldl, K, &map.u, -η2); scaleFcn(ldl, K, &map.v, -η2); @@ -380,11 +380,12 @@ impl LDLDataMap { let Hsblocks = allocate_kkt_Hsblocks::(cones); // now do the sparse cone expansion pieces - let nsparse = cones.iter().filter(|&c| c.is_sparsifiable()).count(); + let nsparse = cones.iter().filter(|&c| c.is_sparse_expandable()).count(); let mut sparse_maps = Vec::with_capacity(nsparse); for cone in cones.iter() { - if let Some(sc) = cone.to_sparse() { + if cone.is_sparse_expandable() { + let sc = cone.to_sparse_expansion().unwrap(); sparse_maps.push(sc.expansion_map()); } } diff --git a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs index b7d410db..57b88951 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs @@ -129,7 +129,8 @@ where let KKT = &mut self.KKT; for cone in cones.iter() { - if let Some(sc) = cone.to_sparse() { + if cone.is_sparse_expandable() { + let sc = cone.to_sparse_expansion().unwrap(); let thismap = sparse_map_iter.next().unwrap(); sc.csc_update_sparsecone(thismap, ldl, KKT, _update_values, _scale_values); } diff --git a/src/solver/core/kktsolvers/direct/quasidef/kkt_assembly.rs b/src/solver/core/kktsolvers/direct/quasidef/kkt_assembly.rs index dbd8d272..cdf6e29b 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/kkt_assembly.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/kkt_assembly.rs @@ -92,7 +92,8 @@ fn _kkt_assemble_colcounts( } //add sparse expansions columns for sparse cones - if let Some(sc) = cone.to_sparse() { + if cone.is_sparse_expandable() { + let sc = cone.to_sparse_expansion().unwrap(); let thismap = sparse_map_iter.next().unwrap(); sc.csc_colcount_sparsecone(thismap, K, row, pcol, shape); pcol += thismap.pdim(); @@ -146,7 +147,8 @@ fn _kkt_assemble_fill( } //add sparse expansions columns for sparse cones - if let Some(sc) = cone.to_sparse() { + if cone.is_sparse_expandable() { + let sc = cone.to_sparse_expansion().unwrap(); let thismap = sparse_map_iter.next().unwrap(); sc.csc_fill_sparsecone(thismap, K, row, pcol, shape); pcol += thismap.pdim(); From 4387dde3a9a656ff110a6fef7d422636876787b7 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Thu, 14 Sep 2023 12:04:51 +0100 Subject: [PATCH 36/52] composite expansion is unreachable --- src/solver/core/cones/compositecone.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/solver/core/cones/compositecone.rs b/src/solver/core/cones/compositecone.rs index 54403d8f..a7cd3ce7 100644 --- a/src/solver/core/cones/compositecone.rs +++ b/src/solver/core/cones/compositecone.rs @@ -183,8 +183,9 @@ where } fn is_sparse_expandable(&self) -> bool { - //This will probably never be called - self.cones.iter().all(|cone| cone.is_sparse_expandable()) + //This should probably never be called + //self.cones.iter().any(|cone| cone.is_sparse_expandable()) + unreachable!(); } fn allows_primal_dual_scaling(&self) -> bool { From 1df16195ab2f311ebe5e5ce2ab2dd82b0a8bc7e8 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Fri, 15 Sep 2023 19:46:51 +0100 Subject: [PATCH 37/52] WIP: for arc test --- src/solver/core/cones/socone.rs | 54 +++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/src/solver/core/cones/socone.rs b/src/solver/core/cones/socone.rs index c8de4008..ac1c82d0 100644 --- a/src/solver/core/cones/socone.rs +++ b/src/solver/core/cones/socone.rs @@ -3,6 +3,7 @@ use crate::{ algebra::*, solver::{core::ScalingStrategy, CoreSettings}, }; +use itertools::izip; // ------------------------------------- // Second order Cone @@ -173,9 +174,27 @@ where let w1sq = w[1..].sumsq(); w[0] = T::sqrt(T::one() + w1sq); + //--------------------- + //DEBUG ALTERNATIVE λ + //λ = Wz. Use inner function here because can't //borrow self and self.λ at the same time - _soc_mul_W_inner(&mut self.λ, z, T::one(), T::zero(), w, self.η); + //_soc_mul_W_inner(&mut self.λ, z, T::one(), T::zero(), w, self.η); + + let phi = T::sqrt(sscale * zscale); + let γ = half * wscale; + self.λ[0] = γ; + self.λ[1..].waxpby( + (γ + z[0] / zscale) / sscale, + &s[1..], + (γ + s[0] / sscale) / zscale, + &z[1..], + ); + self.λ[1..].scale(T::recip(s[0] / sscale + z[0] / zscale + two * γ)); + self.λ.scale(phi); + + //DEBUG ALTERNATIVE λ + //-------------------- if let Some(sparse_data) = &mut self.sparse_data { //various intermediate calcs for u,v,d,η @@ -236,9 +255,14 @@ where } } - fn mul_Hs(&mut self, y: &mut [T], x: &[T], work: &mut [T]) { - self.mul_W(MatrixShape::N, work, x, T::one(), T::zero()); // work = Wx - self.mul_W(MatrixShape::T, y, work, T::one(), T::zero()); // y = c Wᵀwork = W^TWx + fn mul_Hs(&mut self, y: &mut [T], x: &[T], _work: &mut [T]) { + //self.mul_W(MatrixShape::N, work, x, T::one(), T::zero()); // work = Wx + //self.mul_W(MatrixShape::T, y, work, T::one(), T::zero()); // y = c Wᵀwork = W^TWx + let c = self.w.dot(x) * (2.).as_T(); + y.copy_from(x); + y[0] = -x[0]; + y.axpby(c, &self.w, T::one()); + y.scale(self.η * self.η); } fn affine_ds(&self, ds: &mut [T], _s: &[T]) { @@ -249,8 +273,26 @@ where self._combined_ds_shift_symmetric(shift, step_z, step_s, σμ); } - fn Δs_from_Δz_offset(&mut self, out: &mut [T], ds: &[T], work: &mut [T], _z: &[T]) { - self._Δs_from_Δz_offset_symmetric(out, ds, work); + fn Δs_from_Δz_offset(&mut self, out: &mut [T], ds: &[T], _work: &mut [T], z: &[T]) { + //self._Δs_from_Δz_offset_symmetric(out, ds, work); + + let resz = _soc_residual(z); + + let λ1ds1 = self.λ[1..].dot(&ds[1..]); + let w1ds1 = self.w[1..].dot(&ds[1..]); + + out.scalarop_from(|zi| -zi, &z); + out[0] = z[0]; + + let c = self.λ[0] * ds[0] - λ1ds1; + out.scale(c / resz); + + out[0] += self.η * w1ds1; + for (outi, &dsi, &wi) in izip!(out[1..].iter_mut(), &ds[1..], &self.w[1..]) { + *outi += self.η * (dsi + w1ds1 / (T::one() + self.w[0]) * wi); + } + + out.scale(self.λ[0].recip()); } fn step_length( From 7068d536bbb7940d758e9540ee3080f9e5ec62a4 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Sun, 17 Sep 2023 12:33:40 +0100 Subject: [PATCH 38/52] stability improvements for dense SOC --- src/solver/core/cones/socone.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/solver/core/cones/socone.rs b/src/solver/core/cones/socone.rs index ac1c82d0..786decda 100644 --- a/src/solver/core/cones/socone.rs +++ b/src/solver/core/cones/socone.rs @@ -177,11 +177,7 @@ where //--------------------- //DEBUG ALTERNATIVE λ - //λ = Wz. Use inner function here because can't - //borrow self and self.λ at the same time - //_soc_mul_W_inner(&mut self.λ, z, T::one(), T::zero(), w, self.η); - - let phi = T::sqrt(sscale * zscale); + //Compute the scaling point λ. Should satisfy λ = Wz = W^{-T}s let γ = half * wscale; self.λ[0] = γ; self.λ[1..].waxpby( @@ -191,10 +187,7 @@ where &z[1..], ); self.λ[1..].scale(T::recip(s[0] / sscale + z[0] / zscale + two * γ)); - self.λ.scale(phi); - - //DEBUG ALTERNATIVE λ - //-------------------- + self.λ.scale(T::sqrt(sscale * zscale)); if let Some(sparse_data) = &mut self.sparse_data { //various intermediate calcs for u,v,d,η @@ -274,7 +267,8 @@ where } fn Δs_from_Δz_offset(&mut self, out: &mut [T], ds: &[T], _work: &mut [T], z: &[T]) { - //self._Δs_from_Δz_offset_symmetric(out, ds, work); + // out = Wᵀ(λ \ ds). Below is equivalent, + // but appears to be a little more stable let resz = _soc_residual(z); From 178bf283af60dc54c7b4102d1de4e41e48dd2c2a Mon Sep 17 00:00:00 2001 From: Paul Goulart Date: Sun, 17 Sep 2023 14:20:00 +0100 Subject: [PATCH 39/52] Adds generalized power cone (#50) * adds generalized power cones * rewrite of cone traits and sparse cone handling in KKT assembly --- Cargo.toml | 1 + src/algebra/csc/core.rs | 10 +- src/algebra/dense/core.rs | 3 +- src/algebra/floats.rs | 3 +- src/algebra/math_traits.rs | 5 +- src/algebra/vecmath.rs | 7 +- src/solver/core/cones/compositecone.rs | 23 +- src/solver/core/cones/expcone.rs | 171 ++++--- src/solver/core/cones/genpowcone.rs | 482 ++++++++++++++++++ src/solver/core/cones/mod.rs | 15 +- src/solver/core/cones/nonnegativecone.rs | 13 +- ...xppow_common.rs => nonsymmetric_common.rs} | 146 +++--- src/solver/core/cones/powcone.rs | 239 ++++----- src/solver/core/cones/psdtrianglecone.rs | 90 ++-- src/solver/core/cones/socone.rs | 9 +- src/solver/core/cones/supportedcone.rs | 34 +- src/solver/core/cones/zerocone.rs | 8 +- .../kktsolvers/direct/quasidef/datamap.rs | 73 --- .../kktsolvers/direct/quasidef/datamaps.rs | 403 +++++++++++++++ .../direct/quasidef/directldlkktsolver.rs | 71 +-- .../quasidef/{utils.rs => kkt_assembly.rs} | 175 +++---- .../core/kktsolvers/direct/quasidef/mod.rs | 8 +- src/solver/core/solver.rs | 15 +- src/solver/core/traits.rs | 4 +- .../implementations/default/info_print.rs | 6 +- .../implementations/default/kktsystem.rs | 17 +- .../implementations/default/variables.rs | 2 +- tests/basic_genpowcone.rs | 55 ++ tests/basic_powcone.rs | 14 +- tests/basic_sdp.rs | 2 +- 30 files changed, 1480 insertions(+), 624 deletions(-) create mode 100644 src/solver/core/cones/genpowcone.rs rename src/solver/core/cones/{exppow_common.rs => nonsymmetric_common.rs} (64%) delete mode 100644 src/solver/core/kktsolvers/direct/quasidef/datamap.rs create mode 100644 src/solver/core/kktsolvers/direct/quasidef/datamaps.rs rename src/solver/core/kktsolvers/direct/quasidef/{utils.rs => kkt_assembly.rs} (52%) create mode 100644 tests/basic_genpowcone.rs diff --git a/Cargo.toml b/Cargo.toml index 5cd85612..da8d152c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ enum_dispatch = "0.3.8" amd = "0.2.2" thiserror = "1.0" cfg-if = "1.0" +itertools = "0.11" # ------------------------------- # features diff --git a/src/algebra/csc/core.rs b/src/algebra/csc/core.rs index 4dc17b20..934dc06a 100644 --- a/src/algebra/csc/core.rs +++ b/src/algebra/csc/core.rs @@ -69,20 +69,18 @@ where J: IntoIterator, T: FloatT, { + #[allow(clippy::needless_range_loop)] fn from(rows: I) -> CscMatrix { let rows: Vec> = rows .into_iter() - .map(|r| r.into_iter().map(|&v| v).collect()) + .map(|r| r.into_iter().copied().collect()) .collect(); let m = rows.len(); let n = rows.iter().map(|r| r.len()).next().unwrap_or(0); + assert!(rows.iter().all(|r| r.len() == n)); - let nnz = rows - .iter() - .flat_map(|r| r) - .filter(|&&v| v != T::zero()) - .count(); + let nnz = rows.iter().flatten().filter(|&&v| v != T::zero()).count(); let mut colptr = Vec::with_capacity(n + 1); let mut rowval = Vec::with_capacity(nnz); diff --git a/src/algebra/dense/core.rs b/src/algebra/dense/core.rs index 6306d872..bc753f48 100644 --- a/src/algebra/dense/core.rs +++ b/src/algebra/dense/core.rs @@ -42,6 +42,7 @@ pub struct Matrix { /// [0.0, 4.0]]); // ``` // +#[allow(clippy::needless_range_loop)] impl<'a, I, J, T> From for Matrix where I: IntoIterator, @@ -51,7 +52,7 @@ where fn from(rows: I) -> Matrix { let rows: Vec> = rows .into_iter() - .map(|r| r.into_iter().map(|&v| v).collect()) + .map(|r| r.into_iter().copied().collect()) .collect(); let m = rows.len(); diff --git a/src/algebra/floats.rs b/src/algebra/floats.rs index 5156f489..26bed82b 100644 --- a/src/algebra/floats.rs +++ b/src/algebra/floats.rs @@ -1,3 +1,4 @@ +#![allow(non_snake_case)] use num_traits::{Float, FloatConst, FromPrimitive, NumAssign}; use std::fmt::{Debug, Display, LowerExp}; @@ -79,8 +80,6 @@ cfg_if::cfg_if! { // NB: `AsFloatT` is a convenience trait for f32/64 and u32/64 // so that we can do things like (2.0).as_T() everywhere on // constants, rather than the awful T::from_f32(2.0).unwrap() - -#[allow(non_snake_case)] pub trait AsFloatT: 'static { fn as_T(&self) -> T; } diff --git a/src/algebra/math_traits.rs b/src/algebra/math_traits.rs index 95e4a00e..68460c83 100644 --- a/src/algebra/math_traits.rs +++ b/src/algebra/math_traits.rs @@ -96,7 +96,10 @@ pub trait VectorMath { /// Standard Euclidian or 2-norm distance from `self` to `y` fn dist(&self, y: &Self) -> Self::T; - /// Sum of elements squared. + /// Sum of elements. + fn sum(&self) -> Self::T; + + /// Sum of squares of the elements. fn sumsq(&self) -> Self::T; /// 2-norm diff --git a/src/algebra/vecmath.rs b/src/algebra/vecmath.rs index e54a1b52..06425267 100644 --- a/src/algebra/vecmath.rs +++ b/src/algebra/vecmath.rs @@ -1,4 +1,5 @@ use super::{FloatT, ScalarMath, VectorMath}; +use itertools::izip; use std::iter::zip; impl VectorMath for [T] { @@ -88,7 +89,7 @@ impl VectorMath for [T] { assert_eq!(s.len(), ds.len()); let mut out = T::zero(); - for ((&s, &ds), (&z, &dz)) in zip(zip(s, ds), zip(z, dz)) { + for (&s, &ds, &z, &dz) in izip!(s, ds, z, dz) { let si = s + α * ds; let zi = z + α * dz; out += si * zi; @@ -101,6 +102,10 @@ impl VectorMath for [T] { T::sqrt(dist2) } + fn sum(&self) -> T { + self.iter().fold(T::zero(), |acc, &x| acc + x) + } + fn sumsq(&self) -> T { self.dot(self) } diff --git a/src/solver/core/cones/compositecone.rs b/src/solver/core/cones/compositecone.rs index 53a6e376..ab5e4e3a 100644 --- a/src/solver/core/cones/compositecone.rs +++ b/src/solver/core/cones/compositecone.rs @@ -52,7 +52,7 @@ where // create cones with the given dims for t in types.iter() { //make a new cone - let cone = make_cone(*t); + let cone = make_cone(t); //update global problem symmetry _is_symmetric = _is_symmetric && cone.is_symmetric(); @@ -157,7 +157,7 @@ where pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, SupportedCone> { self.cones.iter_mut() } - pub(crate) fn type_count(&self, tag: SupportedConeTag) -> usize { + pub(crate) fn get_type_count(&self, tag: SupportedConeTag) -> usize { if self.type_counts.contains_key(&tag) { self.type_counts[&tag] } else { @@ -182,6 +182,12 @@ where self._is_symmetric } + fn allows_primal_dual_scaling(&self) -> bool { + self.cones + .iter() + .all(|cone| cone.allows_primal_dual_scaling()) + } + fn rectify_equilibration(&self, δ: &mut [T], e: &[T]) -> bool { let mut any_changed = false; @@ -248,14 +254,7 @@ where //This function should probably never be called since //we only us it to interrogate the blocks, but we can //implement something reasonable anyway - let mut is_diag = true; - for cone in self.iter() { - is_diag &= cone.Hs_is_diagonal(); - if !is_diag { - break; - } - } - is_diag + self.cones.iter().all(|cone| cone.Hs_is_diagonal()) } #[allow(non_snake_case)] @@ -348,9 +347,9 @@ where (α, α) } - fn compute_barrier(&self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { + fn compute_barrier(&mut self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { let mut barrier = T::zero(); - for (cone, rng) in zip(&self.cones, &self.rng_cones) { + for (cone, rng) in zip(&mut self.cones, &self.rng_cones) { let zi = &z[rng.clone()]; let si = &s[rng.clone()]; let dzi = &dz[rng.clone()]; diff --git a/src/solver/core/cones/expcone.rs b/src/solver/core/cones/expcone.rs index c38b1506..b8b8c723 100644 --- a/src/solver/core/cones/expcone.rs +++ b/src/solver/core/cones/expcone.rs @@ -8,7 +8,7 @@ use crate::{ // Exponential Cone // ------------------------------------- -pub struct ExponentialCone { +pub struct ExponentialCone { // Hessian of the dual barrier at z H_dual: DenseMatrixSym3, @@ -53,6 +53,10 @@ where false } + fn allows_primal_dual_scaling(&self) -> bool { + true + } + fn rectify_equilibration(&self, δ: &mut [T], e: &[T]) -> bool { δ.copy_from(e).recip().scale(e.mean()); true // scalar equilibration @@ -146,20 +150,18 @@ where ) -> (T, T) { let step = settings.linesearch_backtrack_step; let αmin = settings.min_terminate_step_length; - - // final backtracked position - let wq = &mut [T::zero(); 3]; + let mut work = [T::zero(); 3]; let _is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; let _is_dual_feasible_fcn = |s: &[T]| -> bool { self.is_dual_feasible(s) }; - let αz = self.step_length_3d_cone(wq, dz, z, αmax, αmin, step, _is_dual_feasible_fcn); - let αs = self.step_length_3d_cone(wq, ds, s, αmax, αmin, step, _is_prim_feasible_fcn); + let αz = backtrack_search(dz, z, αmax, αmin, step, _is_dual_feasible_fcn, &mut work); + let αs = backtrack_search(ds, s, αmax, αmin, step, _is_prim_feasible_fcn, &mut work); (αz, αs) } - fn compute_barrier(&self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { + fn compute_barrier(&mut self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { let mut barrier = T::zero(); let cur_z = [z[0] + α * dz[0], z[1] + α * dz[1], z[2] + α * dz[2]]; @@ -172,9 +174,7 @@ where } } -// implement this marker trait to get access to -// functions common to exp and pow cones -impl Nonsymmetric3DCone for ExponentialCone +impl NonsymmetricCone for ExponentialCone where T: FloatT, { @@ -214,46 +214,24 @@ where false } - // Compute the primal gradient of f(s) at s - fn gradient_primal(&self, s: &[T]) -> [T; 3] + fn barrier_primal(&mut self, s: &[T]) -> T where T: FloatT, { - let mut g = [T::zero(); 3]; - let ω = _wright_omega(T::one() - s[0] / s[1] - (s[1] / s[2]).logsafe()); - - g[0] = T::one() / ((ω - T::one()) * s[1]); - g[1] = g[0] + g[0] * ((ω * s[1] / s[2]).logsafe()) - T::one() / s[1]; - g[2] = ω / ((T::one() - ω) * s[2]); - g - } - - fn update_dual_grad_H(&mut self, z: &[T]) { - let grad = &mut self.grad; - let H = &mut self.H_dual; - - // Hessian computation, compute μ locally - let l = (-z[2] / z[0]).logsafe(); - let r = -z[0] * l - z[0] + z[1]; + // Primal barrier: + // f(s) = ⟨s,g(s)⟩ - f*(-g(s)) + // = -2*log(s2) - log(s3) - log((1-barω)^2/barω) - 3, + // where barω = ω(1 - s1/s2 - log(s2) - log(s3)) + // NB: ⟨s,g(s)⟩ = -3 = - ν - // compute the gradient at z - let c2 = r.recip(); + let ω = _wright_omega(T::one() - s[0] / s[1] - (s[1] / s[2]).logsafe()); - grad[0] = c2 * l - z[0].recip(); - grad[1] = -c2; - grad[2] = (c2 * z[0] - T::one()) / z[2]; + let ω = (ω - T::one()) * (ω - T::one()) / ω; - // compute_Hessian(K,z,H). Type is symmetric, so - // only need to assign upper triangle. - H[(0, 0)] = (r * r - z[0] * r + l * l * z[0] * z[0]) / (r * z[0] * z[0] * r); - H[(0, 1)] = -l / (r * r); - H[(1, 1)] = (r * r).recip(); - H[(0, 2)] = (z[1] - z[0]) / (r * r * z[2]); - H[(1, 2)] = -z[0] / (r * r * z[2]); - H[(2, 2)] = (r * r - z[0] * r + z[0] * z[0]) / (r * r * z[2] * z[2]); + -ω.logsafe() - (s[1].logsafe()) * ((2.).as_T()) - s[2].logsafe() - (3.).as_T() } - fn barrier_dual(&self, z: &[T]) -> T + fn barrier_dual(&mut self, z: &[T]) -> T where T: FloatT, { @@ -264,44 +242,7 @@ where -(-z[2] * z[0]).logsafe() - (z[1] - z[0] - z[0] * l).logsafe() } - fn barrier_primal(&self, s: &[T]) -> T - where - T: FloatT, - { - // Primal barrier: - // f(s) = ⟨s,g(s)⟩ - f*(-g(s)) - // = -2*log(s2) - log(s3) - log((1-barω)^2/barω) - 3, - // where barω = ω(1 - s1/s2 - log(s2) - log(s3)) - // NB: ⟨s,g(s)⟩ = -3 = - ν - - let ω = _wright_omega(T::one() - s[0] / s[1] - (s[1] / s[2]).logsafe()); - - let ω = (ω - T::one()) * (ω - T::one()) / ω; - - -ω.logsafe() - (s[1].logsafe()) * ((2.).as_T()) - s[2].logsafe() - (3.).as_T() - } - - // 3rd-order correction at the point z. Output is η. - // - // η = -0.5*[(dot(u,Hψ,v)*ψ - 2*dotψu*dotψv)/(ψ*ψ*ψ)*gψ + - // dotψu/(ψ*ψ)*Hψv + dotψv/(ψ*ψ)*Hψu - dotψuv/ψ + dothuv] - // - // where : - // Hψ = [ 1/z[1] 0 -1/z[3]; - // 0 0 0; - // -1/z[3] 0 z[1]/(z[3]*z[3]);] - // dotψuv = [-u[1]*v[1]/(z[1]*z[1]) + u[3]*v[3]/(z[3]*z[3]); - // 0; - // (u[3]*v[1]+u[1]*v[3])/(z[3]*z[3]) - 2*z[1]*u[3]*v[3]/(z[3]*z[3]*z[3])] - // - // dothuv = [-2*u[1]*v[1]/(z[1]*z[1]*z[1]) ; - // 0; - // -2*u[3]*v[3]/(z[3]*z[3]*z[3])] - // Hψv = Hψ*v - // Hψu = Hψ*u - // gψ is used inside η - - fn higher_correction(&mut self, η: &mut [T; 3], ds: &[T], v: &[T]) + fn higher_correction(&mut self, η: &mut [T], ds: &[T], v: &[T]) where T: FloatT, { @@ -329,8 +270,8 @@ where let ψ = z[0] * η[0] - z[0] + z[1]; - let dotψu = u.dot(&η[..]); - let dotψv = v.dot(&η[..]); + let dotψu = u.dot(η); + let dotψv = v.dot(η); let two: T = (2.).as_T(); let coef = @@ -355,6 +296,70 @@ where η[..].scale((0.5).as_T()); } + // 3rd-order correction at the point z. Output is η. + // + // η = -0.5*[(dot(u,Hψ,v)*ψ - 2*dotψu*dotψv)/(ψ*ψ*ψ)*gψ + + // dotψu/(ψ*ψ)*Hψv + dotψv/(ψ*ψ)*Hψu - dotψuv/ψ + dothuv] + // + // where : + // Hψ = [ 1/z[1] 0 -1/z[3]; + // 0 0 0; + // -1/z[3] 0 z[1]/(z[3]*z[3]);] + // dotψuv = [-u[1]*v[1]/(z[1]*z[1]) + u[3]*v[3]/(z[3]*z[3]); + // 0; + // (u[3]*v[1]+u[1]*v[3])/(z[3]*z[3]) - 2*z[1]*u[3]*v[3]/(z[3]*z[3]*z[3])] + // + // dothuv = [-2*u[1]*v[1]/(z[1]*z[1]*z[1]) ; + // 0; + // -2*u[3]*v[3]/(z[3]*z[3]*z[3])] + // Hψv = Hψ*v + // Hψu = Hψ*u + // gψ is used inside η + + fn update_dual_grad_H(&mut self, z: &[T]) { + let grad = &mut self.grad; + let H = &mut self.H_dual; + + // Hessian computation, compute μ locally + let l = (-z[2] / z[0]).logsafe(); + let r = -z[0] * l - z[0] + z[1]; + + // compute the gradient at z + let c2 = r.recip(); + + grad[0] = c2 * l - z[0].recip(); + grad[1] = -c2; + grad[2] = (c2 * z[0] - T::one()) / z[2]; + + // compute_Hessian(K,z,H). Type is symmetric, so + // only need to assign upper triangle. + H[(0, 0)] = (r * r - z[0] * r + l * l * z[0] * z[0]) / (r * z[0] * z[0] * r); + H[(0, 1)] = -l / (r * r); + H[(1, 1)] = (r * r).recip(); + H[(0, 2)] = (z[1] - z[0]) / (r * r * z[2]); + H[(1, 2)] = -z[0] / (r * r * z[2]); + H[(2, 2)] = (r * r - z[0] * r + z[0] * z[0]) / (r * r * z[2] * z[2]); + } +} + +impl Nonsymmetric3DCone for ExponentialCone +where + T: FloatT, +{ + // Compute the primal gradient of f(s) at s + fn gradient_primal(&self, s: &[T]) -> [T; 3] + where + T: FloatT, + { + let mut g = [T::zero(); 3]; + let ω = _wright_omega(T::one() - s[0] / s[1] - (s[1] / s[2]).logsafe()); + + g[0] = T::one() / ((ω - T::one()) * s[1]); + g[1] = g[0] + g[0] * ((ω * s[1] / s[2]).logsafe()) - T::one() / s[1]; + g[2] = ω / ((T::one() - ω) * s[2]); + g + } + //getters fn split_borrow_mut( &mut self, @@ -428,7 +433,7 @@ where let mut r = z - w - w.logsafe(); // Santiago suggests two refinement iterations only - for _ in 0..3 { + for _ in 0..2 { let wp1 = w + T::one(); let t = wp1 * (wp1 + (r * (2.).as_T()) / (3.0).as_T()); w *= T::one() + (r / wp1) * (t - r * (0.5).as_T()) / (t - r); diff --git a/src/solver/core/cones/genpowcone.rs b/src/solver/core/cones/genpowcone.rs new file mode 100644 index 00000000..992855a9 --- /dev/null +++ b/src/solver/core/cones/genpowcone.rs @@ -0,0 +1,482 @@ +use super::*; +use crate::{ + algebra::*, + solver::{core::ScalingStrategy, CoreSettings}, +}; +use itertools::izip; +use std::iter::zip; + +// ------------------------------------- +// Generalized Power Cone +// ------------------------------------- + +pub struct GenPowerConeData { + // gradient of the dual barrier at z + grad: Vec, + // holds copy of z at scaling point + z: Vec, + + // central path parameter + pub μ: T, + + // vectors for rank 3 update representation of Hs + pub p: Vec, + pub q: Vec, + pub r: Vec, + pub d1: Vec, + + // additional scalar terms for rank-2 rep + d2: T, + // additional constant for initialization in the Newton-Raphson method + ψ: T, + + //work vector length dim, e.g. for line searches + work: Vec, + //work vector exclusively for computing the primal barrier function. + work_pb: Vec, +} + +impl GenPowerConeData +where + T: FloatT, +{ + pub fn new(α: &Vec, dim2: usize) -> Self { + let dim1 = α.len(); + let dim = dim1 + dim2; + + //PJG : these checks belong elsewhere + assert!(α.iter().all(|r| *r > T::zero())); // check all powers are greater than 0 + assert!((T::one() - α.sum()).abs() < (T::epsilon() * α.len().as_T() * (0.5).as_T())); + + Self { + grad: vec![T::zero(); dim], + z: vec![T::zero(); dim], + μ: T::one(), + p: vec![T::zero(); dim], + q: vec![T::zero(); dim1], + r: vec![T::zero(); dim2], + d1: vec![T::zero(); dim1], + d2: T::zero(), + ψ: T::one() / (α.sumsq()), + work: vec![T::zero(); dim], + work_pb: vec![T::zero(); dim], + } + } +} + +pub struct GenPowerCone { + pub α: Vec, // power defining the cone. length determines dim1 + dim2: usize, // dimension of w + pub data: Box>, // Boxed so that the enum_dispatch variant isn't huge +} + +impl GenPowerCone +where + T: FloatT, +{ + pub fn new(α: Vec, dim2: usize) -> Self { + let data = Box::new(GenPowerConeData::::new(&α, dim2)); + Self { α, dim2, data } + } + + pub fn dim1(&self) -> usize { + self.α.len() + } + pub fn dim2(&self) -> usize { + self.dim2 + } + pub fn dim(&self) -> usize { + self.dim1() + self.dim2() + } +} + +impl Cone for GenPowerCone +where + T: FloatT, +{ + fn degree(&self) -> usize { + self.dim1() + 1 + } + + fn numel(&self) -> usize { + self.dim() + } + + fn is_symmetric(&self) -> bool { + false + } + + fn allows_primal_dual_scaling(&self) -> bool { + false + } + + fn rectify_equilibration(&self, δ: &mut [T], e: &[T]) -> bool { + δ.copy_from(e).recip().scale(e.mean()); + true // scalar equilibration + } + + fn margins(&mut self, _z: &mut [T], _pd: PrimalOrDualCone) -> (T, T) { + // We should never end up shifting to this cone, since + // asymmetric problems should always use unit_initialization + unreachable!(); + } + fn scaled_unit_shift(&self, _z: &mut [T], _α: T, _pd: PrimalOrDualCone) { + // We should never end up shifting to this cone, since + // asymmetric problems should always use unit_initialization + unreachable!(); + } + + fn unit_initialization(&self, z: &mut [T], s: &mut [T]) { + let α = &self.α; + let dim1 = self.dim1(); + + s[..dim1].scalarop_from(|αi| T::sqrt(T::one() + αi), α); + s[dim1..].set(T::zero()); + + z.copy_from(s); + } + + fn set_identity_scaling(&mut self) { + // We should never use identity scaling because + // we never want to allow symmetric initialization + unreachable!(); + } + + fn update_scaling( + &mut self, + _s: &[T], + z: &[T], + μ: T, + _scaling_strategy: ScalingStrategy, + ) -> bool { + // update both gradient and Hessian for function f*(z) at the point z + self.update_dual_grad_H(z); + self.data.μ = μ; + + // K.z .= z + self.data.z.copy_from(z); + + true + } + + fn Hs_is_diagonal(&self) -> bool { + true + } + + fn get_Hs(&self, Hsblock: &mut [T]) { + // we are returning here the diagonal D = [d1; d2] block + let dim1 = self.dim1(); + let data = &self.data; + + Hsblock[..dim1].scalarop_from(|d1| data.μ * d1, &data.d1); + Hsblock[dim1..].set(data.μ * data.d2); + } + + fn mul_Hs(&mut self, y: &mut [T], x: &[T], _work: &mut [T]) { + // Hs = μ*(D + pp' -qq' -rr') + + let dim1 = self.dim1(); + let data = &self.data; + + let coef_p = data.p.dot(x); + let coef_q = data.q.dot(&x[..dim1]); + let coef_r = data.r.dot(&x[dim1..]); + + // y1 .= K.d1 .* x1 - coef_q.*K.q + // NB: d1 is a vector + for (y, &x, &d1, &q) in izip!(&mut y[..dim1], &x[..dim1], &data.d1, &data.q) { + *y = d1 * x - coef_q * q; + } + + // y2 .= K.d2 .* x2 - coef_r.*K.r. + // NB: d2 is a scalar + for (y, &x, &r) in izip!(&mut y[dim1..], &x[dim1..], &data.r) { + *y = data.d2 * x - coef_r * r; + } + + y.axpby(coef_p, &data.p, T::one()); + y.scale(data.μ); + } + + fn affine_ds(&self, ds: &mut [T], s: &[T]) { + ds.copy_from(s); + } + + fn combined_ds_shift( + &mut self, shift: &mut [T], _step_z: &mut [T], _step_s: &mut [T], σμ: T + ) { + //YC: No 3rd order correction at present + shift.scalarop_from(|g| g * σμ, &self.data.grad); + } + + fn Δs_from_Δz_offset(&mut self, out: &mut [T], ds: &[T], _work: &mut [T], _z: &[T]) { + out.copy_from(ds); + } + + fn step_length( + &mut self, + dz: &[T], + ds: &[T], + z: &[T], + s: &[T], + settings: &CoreSettings, + αmax: T, + ) -> (T, T) { + let step = settings.linesearch_backtrack_step; + let αmin = settings.min_terminate_step_length; + + //simultaneously using "work" and the closures defined + //below produces a borrow check error, so temporarily + //move "work" out of self + let mut work = std::mem::take(&mut self.data.work); + + let is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; + let is_dual_feasible_fcn = |s: &[T]| -> bool { self.is_dual_feasible(s) }; + + let αz = backtrack_search(dz, z, αmax, αmin, step, is_dual_feasible_fcn, &mut work); + let αs = backtrack_search(ds, s, αmax, αmin, step, is_prim_feasible_fcn, &mut work); + + //restore work to self + self.data.work = work; + + (αz, αs) + } + + fn compute_barrier(&mut self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { + let mut barrier = T::zero(); + let mut work = std::mem::take(&mut self.data.work); + + work.waxpby(T::one(), s, α, ds); + barrier += self.barrier_primal(&work); + + work.waxpby(T::one(), z, α, dz); + barrier += self.barrier_dual(&work); + + self.data.work = work; + + barrier + } +} + +impl NonsymmetricCone for GenPowerCone +where + T: FloatT, +{ + // Returns true if s is primal feasible + fn is_primal_feasible(&self, s: &[T]) -> bool + where + T: FloatT, + { + let α = &self.α; + let two: T = (2f64).as_T(); + let dim1 = self.dim1(); + + if s[..dim1].iter().all(|&x| x > T::zero()) { + let res = zip(α, &s[..dim1]).fold(T::zero(), |res, (&αi, &si)| -> T { + res + two * αi * si.logsafe() + }); + let res = T::exp(res) - s[dim1..].sumsq(); + + if res > T::zero() { + return true; + } + } + false + } + + // Returns true if z is dual feasible + fn is_dual_feasible(&self, z: &[T]) -> bool + where + T: FloatT, + { + let α = &self.α; + let two: T = (2.).as_T(); + let dim1 = self.dim1(); + + if z[..dim1].iter().all(|&x| x > T::zero()) { + let res = zip(α, &z[..dim1]).fold(T::zero(), |res, (&αi, &zi)| -> T { + res + two * αi * (zi / αi).logsafe() + }); + let res = T::exp(res) - z[dim1..].sumsq(); + + if res > T::zero() { + return true; + } + } + false + } + + fn barrier_primal(&mut self, s: &[T]) -> T + where + T: FloatT, + { + // Primal barrier: f(s) = ⟨s,g(s)⟩ - f*(-g(s)) + // NB: ⟨s,g(s)⟩ = -(dim1(K)+1) = - ν + + // can't use "work" here because it was already + // used to construct the argument s in some cases + let mut g = std::mem::take(&mut self.data.work_pb); + + self.gradient_primal(&mut g, s); + g.negate(); //-g(s) + + let out = -self.barrier_dual(&g) - self.degree().as_T(); + + self.data.work_pb = g; + + out + } + + fn barrier_dual(&mut self, z: &[T]) -> T + where + T: FloatT, + { + // Dual barrier: + let α = &self.α; + let dim1 = self.dim1(); + let two: T = (2.).as_T(); + + let mut res = T::zero(); + for (&zi, &αi) in zip(&z[..dim1], α) { + res += two * αi * (zi / αi).logsafe(); + } + res = T::exp(res) - z[dim1..].sumsq(); + + let mut barrier: T = -res.logsafe(); + for (&zi, &αi) in zip(&z[..dim1], α) { + barrier -= (zi).logsafe() * (T::one() - αi); + } + + barrier + } + + fn higher_correction(&mut self, _η: &mut [T], _ds: &[T], _v: &[T]) { + unimplemented!() + } + + fn update_dual_grad_H(&mut self, z: &[T]) { + let α = &self.α; + let dim1 = self.dim1(); + let data = &mut self.data; + let two: T = (2.).as_T(); + + let phi = zip(α, z).fold(T::one(), |phi, (&αi, &zi)| phi * (zi / αi).powf(two * αi)); + + let norm2w = z[dim1..].sumsq(); + let ζ = phi - norm2w; + assert!(ζ > T::zero()); + + // compute the gradient at z + let grad = &mut data.grad; + let τ = &mut data.q; + + for (τ, grad, &α, &z) in izip!(τ.iter_mut(), &mut grad[..dim1], α, &z[..dim1]) { + *τ = two * α / z; + *grad = -(*τ) * phi / ζ - (T::one() - α) / z; + } + + grad[dim1..].scalarop_from(|z| (two / ζ) * z, &z[dim1..]); + + // compute Hessian information at z + let p0 = T::sqrt(phi * (phi + norm2w) / two); + let p1 = -two * phi / p0; + let q0 = T::sqrt(ζ * phi / two); + let r1 = two * T::sqrt(ζ / (phi + norm2w)); + + // compute the diagonal d1,d2 + for (d1, &τ, &α, &z) in izip!(&mut data.d1, τ.iter(), α, &z[..dim1]) { + *d1 = (τ) * phi / (ζ * z) + (T::one() - α) / (z * z); + } + data.d2 = two / ζ; + + // compute p, q, r where τ shares memory with q + data.p[..dim1].scalarop_from(|τi| (p0 / ζ) * τi, τ); + data.p[dim1..].scalarop_from(|zi| (p1 / ζ) * zi, &z[dim1..]); + + data.q.scale(q0 / ζ); + data.r.scalarop_from(|zi| (r1 / ζ) * zi, &z[dim1..]); + } +} + +impl NonsymmetricNDCone for GenPowerCone +where + T: FloatT, +{ + // Compute the primal gradient of f(s) at s + fn gradient_primal(&self, g: &mut [T], s: &[T]) + where + T: FloatT, + { + let dim1 = self.dim1(); + let two: T = (2.).as_T(); + let data = &self.data; + + // unscaled phi + let phi = + zip(&s[..dim1], &self.α).fold(T::one(), |phi, (&si, &αi)| phi * si.powf(two * αi)); + + // obtain g1 from the Newton-Raphson method + let (p, r) = s.split_at(dim1); + let (gp, gr) = g.split_at_mut(dim1); + let norm_r = r.norm(); + + if norm_r > T::epsilon() { + let g1 = _newton_raphson_genpowcone(norm_r, p, phi, &self.α, data.ψ); + + gr.scalarop_from(|r| (g1 / norm_r) * r, &data.r); + + for (gp, &α, &p) in izip!(gp.iter_mut(), &self.α, p) { + *gp = -(T::one() + α + α * g1 * norm_r) / p; + } + } else { + gr.set(T::zero()); + + for (gp, &α, &p) in izip!(gp.iter_mut(), &self.α, p) { + *gp = -(T::one() + α) / p; + } + } + } +} +// ---------------------------------------------- +// internal operations for generalized power cones + +// Newton-Raphson method: +// solve a one-dimensional equation f(x) = 0 +// x(k+1) = x(k) - f(x(k))/f'(x(k)) +// When we initialize x0 such that 0 < x0 < x* and f(x0) > 0, +// the Newton-Raphson method converges quadratically + +fn _newton_raphson_genpowcone(norm_r: T, p: &[T], phi: T, α: &[T], ψ: T) -> T +where + T: FloatT, +{ + let two: T = (2.).as_T(); + + // init point x0: f(x0) > 0 + let x0 = -norm_r.recip() + + (ψ * norm_r + ((phi / norm_r / norm_r + ψ * ψ - T::one()) * phi).sqrt()) + / (phi - norm_r * norm_r); + + // function for f(x) = 0 + let f0 = { + |x: T| -> T { + let finit = -(two * x / norm_r + x * x).logsafe(); + + zip(α, p).fold(finit, |f, (&αi, &pi)| { + f + two * αi * ((x * norm_r + (T::one() + αi) / αi).logsafe() - pi.logsafe()) + }) + } + }; + + // first derivative + let f1 = { + |x: T| -> T { + let finit = -(two * x + two / norm_r) / (x * x + two * x / norm_r); + + α.iter().fold(finit, |f, &αi| { + f + two * (αi) * norm_r / (norm_r * x + (T::one() + αi) / αi) + }) + } + }; + newton_raphson_onesided(x0, f0, f1) +} diff --git a/src/solver/core/cones/mod.rs b/src/solver/core/cones/mod.rs index caa31c5e..f8219561 100644 --- a/src/solver/core/cones/mod.rs +++ b/src/solver/core/cones/mod.rs @@ -10,19 +10,20 @@ mod compositecone; mod supportedcone; // primitive cone types mod expcone; +mod genpowcone; mod nonnegativecone; mod powcone; mod socone; mod zerocone; // partially specialized traits and blanket implementataions -mod exppow_common; +mod nonsymmetric_common; mod symmetric_common; //re-export everything to appear as one module -use exppow_common::*; +use nonsymmetric_common::*; pub use { - compositecone::*, expcone::*, nonnegativecone::*, powcone::*, socone::*, supportedcone::*, - symmetric_common::*, zerocone::*, + compositecone::*, expcone::*, genpowcone::*, nonnegativecone::*, powcone::*, socone::*, + supportedcone::*, symmetric_common::*, zerocone::*, }; // only use PSD cones with SDP/Blas enabled @@ -47,8 +48,12 @@ where fn degree(&self) -> usize; fn numel(&self) -> usize; + // is the cone symmetric? NB: zero cone still reports true fn is_symmetric(&self) -> bool; + // report false here if only dual scaling is implemented (e.g. GenPowerCone) + fn allows_primal_dual_scaling(&self) -> bool; + // converts an elementwise scaling into // a scaling that preserves cone memership fn rectify_equilibration(&self, δ: &mut [T], e: &[T]) -> bool; @@ -142,5 +147,5 @@ where ) -> (T, T); // return the barrier function at (z+αdz,s+αds) - fn compute_barrier(&self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T; + fn compute_barrier(&mut self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T; } diff --git a/src/solver/core/cones/nonnegativecone.rs b/src/solver/core/cones/nonnegativecone.rs index 55bb4f8d..bd808658 100644 --- a/src/solver/core/cones/nonnegativecone.rs +++ b/src/solver/core/cones/nonnegativecone.rs @@ -3,13 +3,14 @@ use crate::{ algebra::*, solver::{core::ScalingStrategy, CoreSettings}, }; +use itertools::izip; use std::iter::zip; // ------------------------------------- // Nonnegative Cone // ------------------------------------- -pub struct NonnegativeCone { +pub struct NonnegativeCone { dim: usize, w: Vec, λ: Vec, @@ -44,6 +45,10 @@ where true } + fn allows_primal_dual_scaling(&self) -> bool { + true + } + fn rectify_equilibration(&self, δ: &mut [T], _e: &[T]) -> bool { δ.set(T::one()); false @@ -75,7 +80,7 @@ where _μ: T, _scaling_strategy: ScalingStrategy, ) -> bool { - for ((λ, w), (s, z)) in zip(zip(&mut self.λ, &mut self.w), zip(s, z)) { + for (λ, w, s, z) in izip!(&mut self.λ, &mut self.w, s, z) { *λ = T::sqrt((*s) * (*z)); *w = T::sqrt((*s) / (*z)); } @@ -146,12 +151,12 @@ where (αz, αs) } - fn compute_barrier(&self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { + fn compute_barrier(&mut self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { assert_eq!(z.len(), s.len()); assert_eq!(dz.len(), z.len()); assert_eq!(ds.len(), s.len()); let mut barrier = T::zero(); - for ((&s, &ds), (&z, &dz)) in zip(zip(s, ds), zip(z, dz)) { + for (&s, &ds, &z, &dz) in izip!(s, ds, z, dz) { let si = s + α * ds; let zi = z + α * dz; barrier += (si * zi).logsafe(); diff --git a/src/solver/core/cones/exppow_common.rs b/src/solver/core/cones/nonsymmetric_common.rs similarity index 64% rename from src/solver/core/cones/exppow_common.rs rename to src/solver/core/cones/nonsymmetric_common.rs index 5ae6689b..168e65c6 100644 --- a/src/solver/core/cones/exppow_common.rs +++ b/src/solver/core/cones/nonsymmetric_common.rs @@ -1,31 +1,33 @@ use crate::{algebra::*, solver::core::ScalingStrategy}; // -------------------------------------- -// Traits and blanket implementations for Exponential and PowerCones +// Traits and blanket implementations for Exponential, 3D Power and ND Power Cones // ------------------------------------- - -// Operations supported on 3d nonsymmetrics only -pub(crate) trait Nonsymmetric3DCone { +// Operations supported on all nonsymmetric cones +pub(crate) trait NonsymmetricCone { // Returns true if s is primal feasible fn is_primal_feasible(&self, s: &[T]) -> bool; // Returns true if z is dual feasible fn is_dual_feasible(&self, z: &[T]) -> bool; - // Compute the primal gradient of f(s) at s - fn gradient_primal(&self, s: &[T]) -> [T; 3]; + fn barrier_primal(&mut self, s: &[T]) -> T; - fn update_dual_grad_H(&mut self, z: &[T]); + fn barrier_dual(&mut self, z: &[T]) -> T; - fn barrier_dual(&self, z: &[T]) -> T; + fn higher_correction(&mut self, η: &mut [T], ds: &[T], v: &[T]); - fn barrier_primal(&self, s: &[T]) -> T; + fn update_dual_grad_H(&mut self, z: &[T]); +} - fn higher_correction(&mut self, η: &mut [T; 3], ds: &[T], v: &[T]); +// -------------------------------------- +// Trait and blanket utlity implementations for Exponential and 3D Power Cones +// ------------------------------------- +#[allow(clippy::too_many_arguments)] + +pub(crate) trait Nonsymmetric3DCone { + fn gradient_primal(&self, s: &[T]) -> [T; 3]; - // we can't mutably borrow individual fields through getter methods, - // so we have this one method to borrow them simultaneously. - // Usage: let (H_dual, Hs, grad, z) = self.split_borrow_mut (); fn split_borrow_mut( &mut self, ) -> ( @@ -36,24 +38,12 @@ pub(crate) trait Nonsymmetric3DCone { ); } -#[allow(clippy::too_many_arguments)] pub(crate) trait Nonsymmetric3DConeUtils { fn update_Hs(&mut self, s: &[T], z: &[T], μ: T, scaling_strategy: ScalingStrategy); fn use_dual_scaling(&mut self, μ: T); fn use_primal_dual_scaling(&mut self, s: &[T], z: &[T]); - - fn step_length_3d_cone( - &self, - wq: &mut [T], - dq: &[T], - q: &[T], - α_init: T, - α_min: T, - backtrack: T, - is_in_cone_fcn: impl Fn(&[T]) -> bool, - ) -> T; } impl Nonsymmetric3DConeUtils for C @@ -61,14 +51,6 @@ where T: FloatT, C: Nonsymmetric3DCone, { - // find the maximum step length α≥0 so that - // q + α*dq stays in an exponential or power - // cone, or their respective dual cones. - // - // NB: Not for use as a general checking - // function because cone lengths are hardcoded - // to R^3 for faster execution. - fn update_Hs(&mut self, s: &[T], z: &[T], μ: T, scaling_strategy: ScalingStrategy) { // Choose the scaling strategy if scaling_strategy == ScalingStrategy::Dual { @@ -159,34 +141,80 @@ where self.use_dual_scaling(μ); } } +} - fn step_length_3d_cone( - &self, - wq: &mut [T], - dq: &[T], - q: &[T], - α_init: T, - α_min: T, - backtrack: T, - is_in_cone_fcn: impl Fn(&[T]) -> bool, - ) -> T { - let mut α = α_init; - - loop { - // wq = q + α*dq - for i in 0..3 { - wq[i] = q[i] + α * dq[i]; - } +// -------------------------------------- +// Traits for general ND cones +// ------------------------------------- - if is_in_cone_fcn(wq) { - break; - } - α *= backtrack; - if α < α_min { - α = T::zero(); - break; - } +// Operations supported on ND nonsymmetrics only. Note this +// differs from the 3D cone in particular because we don't +// return a 3D tuple for the primal gradient. +pub(crate) trait NonsymmetricNDCone { + // Compute the primal gradient of f(s) at s + fn gradient_primal(&self, grad: &mut [T], s: &[T]); +} + +// -------------------------------------- +// utility functions for nonsymmetric cones +// -------------------------------------- + +// find the maximum step length α≥0 so that +// q + α*dq stays in an exponential or power +// cone, or their respective dual cones. +pub(crate) fn backtrack_search( + dq: &[T], + q: &[T], + α_init: T, + α_min: T, + step: T, + is_in_cone_fcn: impl Fn(&[T]) -> bool, + work: &mut [T], +) -> T +where + T: FloatT, +{ + let mut α = α_init; + + loop { + // work = q + α*dq + work.waxpby(T::one(), q, α, dq); + + if is_in_cone_fcn(work) { + break; + } + α *= step; + if α < α_min { + α = T::zero(); + break; } - α } + α +} +pub(crate) fn newton_raphson_onesided(x0: T, f0: impl Fn(T) -> T, f1: impl Fn(T) -> T) -> T +where + T: FloatT, +{ + // implements NR method from a starting point assumed to be to the + // left of the true value. Once a negative step is encountered + // this function will halt regardless of the calculated correction. + + let mut x = x0; + let mut iter = 0; + + while iter < 100 { + iter += 1; + let dfdx = f1(x); + let dx = -f0(x) / dfdx; + + if (dx < T::epsilon()) + || (T::abs(dx / x) < T::sqrt(T::epsilon())) + || (T::abs(dfdx) < T::epsilon()) + { + break; + } + x += dx; + } + + x } diff --git a/src/solver/core/cones/powcone.rs b/src/solver/core/cones/powcone.rs index 59a92a60..99085cd7 100644 --- a/src/solver/core/cones/powcone.rs +++ b/src/solver/core/cones/powcone.rs @@ -8,7 +8,7 @@ use crate::{ // Power Cone // ------------------------------------- -pub struct PowerCone { +pub struct PowerCone { // power defining the cone α: T, // Hessian of the dual barrier at z @@ -55,6 +55,10 @@ where false } + fn allows_primal_dual_scaling(&self) -> bool { + true + } + fn rectify_equilibration(&self, δ: &mut [T], e: &[T]) -> bool { δ.copy_from(e).recip().scale(e.mean()); true // scalar equilibration @@ -149,20 +153,18 @@ where ) -> (T, T) { let step = settings.linesearch_backtrack_step; let αmin = settings.min_terminate_step_length; - - // final backtracked position - let mut wq = [T::zero(); 3]; + let mut work = [T::zero(); 3]; let _is_prim_feasible_fcn = |s: &[T]| -> bool { self.is_primal_feasible(s) }; let _is_dual_feasible_fcn = |s: &[T]| -> bool { self.is_dual_feasible(s) }; - let αz = self.step_length_3d_cone(&mut wq, dz, z, αmax, αmin, step, _is_dual_feasible_fcn); - let αs = self.step_length_3d_cone(&mut wq, ds, s, αmax, αmin, step, _is_prim_feasible_fcn); + let αz = backtrack_search(dz, z, αmax, αmin, step, _is_dual_feasible_fcn, &mut work); + let αs = backtrack_search(ds, s, αmax, αmin, step, _is_prim_feasible_fcn, &mut work); (αz, αs) } - fn compute_barrier(&self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { + fn compute_barrier(&mut self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { let mut barrier = T::zero(); let cur_z = [z[0] + α * dz[0], z[1] + α * dz[1], z[2] + α * dz[2]]; @@ -179,7 +181,7 @@ where // primal-dual scaling //------------------------------------- -impl Nonsymmetric3DCone for PowerCone +impl NonsymmetricCone for PowerCone where T: FloatT, { @@ -220,70 +222,30 @@ where false } - // Compute the primal gradient of f(s) at s - fn gradient_primal(&self, s: &[T]) -> [T; 3] + fn barrier_primal(&mut self, s: &[T]) -> T where T: FloatT, { - let α = self.α; - let mut g = [T::zero(); 3]; - let two: T = (2.).as_T(); - - // unscaled ϕ - let phi = (s[0]).powf(two * α) * (s[1]).powf(two - α * two); - - // obtain last element of g from the Newton-Raphson method - let abs_s = s[2].abs(); - if abs_s > T::epsilon() { - g[2] = _newton_raphson_powcone(abs_s, phi, α); - if s[2] < T::zero() { - g[2] = -g[2]; - } - g[0] = -(α * g[2] * s[2] + T::one() + α) / s[0]; - g[1] = -((T::one() - α) * g[2] * s[2] + two - α) / s[1]; - } else { - g[2] = T::zero(); - g[0] = -(T::one() + α) / s[0]; - g[1] = -(two - α) / s[1]; - } - g - } + // Primal barrier: f(s) = ⟨s,g(s)⟩ - f*(-g(s)) + // NB: ⟨s,g(s)⟩ = -3 = - ν - fn update_dual_grad_H(&mut self, z: &[T]) { - let H = &mut self.H_dual; let α = self.α; let two: T = (2.).as_T(); - let four: T = (4.).as_T(); - - let phi = (z[0] / α).powf(two * α) * (z[1] / (T::one() - α)).powf(two - two * α); - let ψ = phi - z[2] * z[2]; + let three: T = (3.).as_T(); - // use K.grad as a temporary workspace - let gψ = &mut self.grad; - gψ[0] = two * α * phi / (z[0] * ψ); - gψ[1] = two * (T::one() - α) * phi / (z[1] * ψ); - gψ[2] = -two * z[2] / ψ; + let g = self.gradient_primal(s); - // compute_Hessian(K,z,H). Type is symmetric, so - // only need to assign upper triangle. - H[(0, 0)] = gψ[0] * gψ[0] - two * α * (two * α - T::one()) * phi / (z[0] * z[0] * ψ) - + (T::one() - α) / (z[0] * z[0]); - H[(0, 1)] = gψ[0] * gψ[1] - four * α * (T::one() - α) * phi / (z[0] * z[1] * ψ); - H[(1, 1)] = gψ[1] * gψ[1] - - two * (T::one() - α) * (T::one() - two * α) * phi / (z[1] * z[1] * ψ) - + α / (z[1] * z[1]); - H[(0, 2)] = gψ[0] * gψ[2]; - H[(1, 2)] = gψ[1] * gψ[2]; - H[(2, 2)] = gψ[2] * gψ[2] + two / ψ; + let mut out = T::zero(); - // compute the gradient at z - let grad = &mut self.grad; - grad[0] = -two * α * phi / (z[0] * ψ) - (T::one() - α) / z[0]; - grad[1] = -two * (T::one() - α) * phi / (z[1] * ψ) - α / z[1]; - grad[2] = two * z[2] / ψ; + out += ((-g[0] / α).powf(two * α) * (-g[1] / (T::one() - α)).powf(two - α * two) + - g[2] * g[2]) + .logsafe(); + out += (T::one() - α) * (-g[0]).logsafe(); + out += α * (-g[1]).logsafe() - three; + out } - fn barrier_dual(&self, z: &[T]) -> T + fn barrier_dual(&mut self, z: &[T]) -> T where T: FloatT, { @@ -297,41 +259,7 @@ where -arg1.logsafe() - (T::one() - α) * z[0].logsafe() - α * z[1].logsafe() } - fn barrier_primal(&self, s: &[T]) -> T - where - T: FloatT, - { - // Primal barrier: f(s) = ⟨s,g(s)⟩ - f*(-g(s)) - // NB: ⟨s,g(s)⟩ = -3 = - ν - - let α = self.α; - let two: T = (2.).as_T(); - let three: T = (3.).as_T(); - - let g = self.gradient_primal(s); - - let mut out = T::zero(); - - out += ((-g[0] / α).powf(two * α) * (-g[1] / (T::one() - α)).powf(two - α * two) - - g[2] * g[2]) - .logsafe(); - out += (T::one() - α) * (-g[0]).logsafe(); - out += α * (-g[1]).logsafe() - three; - out - } - - // 3rd-order correction at the point z. Output is η. - // - // 3rd order correction: - // η = -0.5*[(dot(u,Hψ,v)*ψ - 2*dotψu*dotψv)/(ψ*ψ*ψ)*gψ + - // dotψu/(ψ*ψ)*Hψv + dotψv/(ψ*ψ)*Hψu - - // dotψuv/ψ + dothuv] - // where: - // Hψ = [ 2*α*(2*α-1)*ϕ/(z1*z1) 4*α*(1-α)*ϕ/(z1*z2) 0; - // 4*α*(1-α)*ϕ/(z1*z2) 2*(1-α)*(1-2*α)*ϕ/(z2*z2) 0; - // 0 0 -2;] - - fn higher_correction(&mut self, η: &mut [T; 3], ds: &[T], v: &[T]) + fn higher_correction(&mut self, η: &mut [T], ds: &[T], v: &[T]) where T: FloatT, { @@ -376,8 +304,8 @@ where Hψ[(1, 2)] = T::zero(); Hψ[(2, 2)] = -two; - let dotψu = u.dot(&η[..]); - let dotψv = v.dot(&η[..]); + let dotψu = u.dot(η); + let dotψv = v.dot(η); let mut Hψv = [T::zero(); 3]; Hψ.mul(&mut Hψv, v); @@ -411,6 +339,85 @@ where η[..].scale((0.5).as_T()); } + // 3rd-order correction at the point z. Output is η. + // + // 3rd order correction: + // η = -0.5*[(dot(u,Hψ,v)*ψ - 2*dotψu*dotψv)/(ψ*ψ*ψ)*gψ + + // dotψu/(ψ*ψ)*Hψv + dotψv/(ψ*ψ)*Hψu - + // dotψuv/ψ + dothuv] + // where: + // Hψ = [ 2*α*(2*α-1)*ϕ/(z1*z1) 4*α*(1-α)*ϕ/(z1*z2) 0; + // 4*α*(1-α)*ϕ/(z1*z2) 2*(1-α)*(1-2*α)*ϕ/(z2*z2) 0; + // 0 0 -2;] + + fn update_dual_grad_H(&mut self, z: &[T]) { + let H = &mut self.H_dual; + let α = self.α; + let two: T = (2.).as_T(); + let four: T = (4.).as_T(); + + let phi = (z[0] / α).powf(two * α) * (z[1] / (T::one() - α)).powf(two - two * α); + let ψ = phi - z[2] * z[2]; + + // use K.grad as a temporary workspace + let gψ = &mut self.grad; + gψ[0] = two * α * phi / (z[0] * ψ); + gψ[1] = two * (T::one() - α) * phi / (z[1] * ψ); + gψ[2] = -two * z[2] / ψ; + + // compute_Hessian(K,z,H). Type is symmetric, so + // only need to assign upper triangle. + H[(0, 0)] = gψ[0] * gψ[0] - two * α * (two * α - T::one()) * phi / (z[0] * z[0] * ψ) + + (T::one() - α) / (z[0] * z[0]); + H[(0, 1)] = gψ[0] * gψ[1] - four * α * (T::one() - α) * phi / (z[0] * z[1] * ψ); + H[(1, 1)] = gψ[1] * gψ[1] + - two * (T::one() - α) * (T::one() - two * α) * phi / (z[1] * z[1] * ψ) + + α / (z[1] * z[1]); + H[(0, 2)] = gψ[0] * gψ[2]; + H[(1, 2)] = gψ[1] * gψ[2]; + H[(2, 2)] = gψ[2] * gψ[2] + two / ψ; + + // compute the gradient at z + let grad = &mut self.grad; + grad[0] = -two * α * phi / (z[0] * ψ) - (T::one() - α) / z[0]; + grad[1] = -two * (T::one() - α) * phi / (z[1] * ψ) - α / z[1]; + grad[2] = two * z[2] / ψ; + } +} + +impl Nonsymmetric3DCone for PowerCone +where + T: FloatT, +{ + // Compute the primal gradient of f(s) at s + fn gradient_primal(&self, s: &[T]) -> [T; 3] + where + T: FloatT, + { + let α = self.α; + let mut g = [T::zero(); 3]; + let two: T = (2.).as_T(); + + // unscaled ϕ + let phi = (s[0]).powf(two * α) * (s[1]).powf(two - α * two); + + // obtain last element of g from the Newton-Raphson method + let abs_s = s[2].abs(); + if abs_s > T::epsilon() { + g[2] = _newton_raphson_powcone(abs_s, phi, α); + if s[2] < T::zero() { + g[2] = -g[2]; + } + g[0] = -(α * g[2] * s[2] + T::one() + α) / s[0]; + g[1] = -((T::one() - α) * g[2] * s[2] + two - α) / s[1]; + } else { + g[2] = T::zero(); + g[0] = -(T::one() + α) / s[0]; + g[1] = -(two - α) / s[1]; + } + g + } + //getters fn split_borrow_mut( &mut self, @@ -442,13 +449,13 @@ where { let two: T = (2.).as_T(); let three: T = (3.).as_T(); - let four: T = (4.).as_T(); // init point x0: since our dual barrier has an additional // shift -2α*log(α) - 2(1-α)*log(1-α) > 0 in f(x), // the previous selection is still feasible, i.e. f(x0) > 0 - let x0 = -s3.recip() - + (s3 + ((phi * four) * phi / s3 / s3 + three * phi).sqrt()) * two / (phi * four - s3 * s3); + + let x0 = + -s3.recip() + (s3 * two + T::sqrt((phi * phi) / (s3 * s3) + phi * three)) / (phi - s3 * s3); // additional shift due to the choice of dual barrier let t0 = -two * α * (α.logsafe()) - two * (T::one() - α) * (T::one() - α).logsafe(); @@ -479,33 +486,5 @@ where - ((x + s3.recip()) * two) / (t1 + t2) } }; - _newton_raphson_onesided(x0, f0, f1) -} - -fn _newton_raphson_onesided(x0: T, f0: impl Fn(T) -> T, f1: impl Fn(T) -> T) -> T -where - T: FloatT, -{ - // implements NR method from a starting point assumed to be to the - // left of the true value. Once a negative step is encountered - // this function will halt regardless of the calculated correction. - - let mut x = x0; - let mut iter = 0; - - while iter < 100 { - iter += 1; - let dfdx = f1(x); - let dx = -f0(x) / dfdx; - - if (dx < T::epsilon()) - || (T::abs(dx / x) < T::sqrt(T::epsilon())) - || (T::abs(dfdx) < T::epsilon()) - { - break; - } - x += dx; - } - - x + newton_raphson_onesided(x0, f0, f1) } diff --git a/src/solver/core/cones/psdtrianglecone.rs b/src/solver/core/cones/psdtrianglecone.rs index f1f96de8..26e490f9 100644 --- a/src/solver/core/cones/psdtrianglecone.rs +++ b/src/solver/core/cones/psdtrianglecone.rs @@ -8,7 +8,7 @@ use crate::{ // Positive Semidefinite Cone (Scaled triangular form) // ------------------------------------ -pub struct PSDConeWork { +pub struct PSDConeData { cholS: CholeskyEngine, cholZ: CholeskyEngine, SVD: SVDEngine, @@ -28,7 +28,7 @@ pub struct PSDConeWork { workvec: Vec, } -impl PSDConeWork +impl PSDConeData where T: FloatT, { @@ -59,9 +59,9 @@ where } pub struct PSDTriangleCone { - n: usize, // matrix dimension, i.e. matrix is n /times n - numel: usize, // total number of elements (lower triangle of) the matrix - work: Box>, // Boxed so that the PSDCone enum_dispatch variant isn't huge + n: usize, // matrix dimension, i.e. matrix is n × n + numel: usize, // total number of elements in (lower triangle of) the matrix + data: Box>, // Boxed so that the PSDCone enum_dispatch variant isn't huge } impl PSDTriangleCone @@ -73,7 +73,7 @@ where Self { n, numel: triangular_number(n), - work: Box::new(PSDConeWork::::new(n)), + data: Box::new(PSDConeData::::new(n)), } } } @@ -94,6 +94,10 @@ where true } + fn allows_primal_dual_scaling(&self) -> bool { + true + } + fn rectify_equilibration(&self, δ: &mut [T], e: &[T]) -> bool { δ.copy_from(e).recip().scale(e.mean()); true // scalar equilibration @@ -108,10 +112,10 @@ where α = T::max_value(); e = &[T::zero(); 0]; } else { - let Z = &mut self.work.workmat1; + let Z = &mut self.data.workmat1; _svec_to_mat(Z, z); - self.work.Eig.eigvals(Z).expect("Eigval error"); - e = &self.work.Eig.λ; + self.data.Eig.eigvals(Z).expect("Eigval error"); + e = &self.data.Eig.λ; α = e.minimum(); } @@ -135,9 +139,9 @@ where } fn set_identity_scaling(&mut self) { - self.work.R.set_identity(); - self.work.Rinv.set_identity(); - self.work.Hs.set_identity(); + self.data.R.set_identity(); + self.data.Rinv.set_identity(); + self.data.Hs.set_identity(); } fn update_scaling( @@ -152,7 +156,7 @@ where return true; } - let f = &mut self.work; + let f = &mut self.data; let (S, Z) = (&mut f.workmat1, &mut f.workmat2); _svec_to_mat(S, s); _svec_to_mat(Z, z); @@ -204,7 +208,7 @@ where } fn get_Hs(&self, Hsblock: &mut [T]) { - self.work.Hs.sym().pack_triu(Hsblock); + self.data.Hs.sym().pack_triu(Hsblock); } fn mul_Hs(&mut self, y: &mut [T], x: &[T], work: &mut [T]) { @@ -216,7 +220,7 @@ where fn affine_ds(&self, ds: &mut [T], _s: &[T]) { ds.set(T::zero()); for k in 0..self.n { - ds[triangular_index(k)] = self.work.λ[k] * self.work.λ[k]; + ds[triangular_index(k)] = self.data.λ[k] * self.data.λ[k]; } } @@ -237,9 +241,9 @@ where _settings: &CoreSettings, αmax: T, ) -> (T, T) { - let Λisqrt = &self.work.Λisqrt; - let d = &mut self.work.workvec; - let engine = &mut self.work.Eig; + let Λisqrt = &self.data.Λisqrt; + let d = &mut self.data.workvec; + let engine = &mut self.data.Eig; // d = Δz̃ = WΔz _mul_Wx_inner( @@ -248,12 +252,12 @@ where dz, T::one(), T::zero(), - &self.work.R, - &mut self.work.workmat1, - &mut self.work.workmat2, - &mut self.work.workmat3, + &self.data.R, + &mut self.data.workmat1, + &mut self.data.workmat2, + &mut self.data.workmat3, ); - let workΔ = &mut self.work.workmat1; + let workΔ = &mut self.data.workmat1; let αz = _step_length_psd_component(workΔ, engine, d, Λisqrt, αmax); // d = Δs̃ = W^{-T}Δs @@ -263,18 +267,18 @@ where ds, T::one(), T::zero(), - &self.work.Rinv, - &mut self.work.workmat1, - &mut self.work.workmat2, - &mut self.work.workmat3, + &self.data.Rinv, + &mut self.data.workmat1, + &mut self.data.workmat2, + &mut self.data.workmat3, ); - let workΔ = &mut self.work.workmat1; + let workΔ = &mut self.data.workmat1; let αs = _step_length_psd_component(workΔ, engine, d, Λisqrt, αmax); (αz, αs) } - fn compute_barrier(&self, _z: &[T], _s: &[T], _dz: &[T], _ds: &[T], _α: T) -> T { + fn compute_barrier(&mut self, _z: &[T], _s: &[T], _dz: &[T], _ds: &[T], _α: T) -> T { // We should return this, but in a smarter way. // This is not yet implemented, but would only // be required for problems mixing PSD and @@ -295,13 +299,13 @@ where { // implements x = λ \ z for the SDP cone fn λ_inv_circ_op(&mut self, x: &mut [T], z: &[T]) { - let X = &mut self.work.workmat1; - let Z = &mut self.work.workmat2; + let X = &mut self.data.workmat1; + let Z = &mut self.data.workmat2; _svec_to_mat(X, x); _svec_to_mat(Z, z); - let λ = &self.work.λ; + let λ = &self.data.λ; let two: T = (2.).as_T(); for i in 0..self.n { for j in 0..self.n { @@ -318,10 +322,10 @@ where x, α, β, - &self.work.R, - &mut self.work.workmat1, - &mut self.work.workmat2, - &mut self.work.workmat3, + &self.data.R, + &mut self.data.workmat1, + &mut self.data.workmat2, + &mut self.data.workmat3, ) } @@ -332,10 +336,10 @@ where x, α, β, - &self.work.Rinv, - &mut self.work.workmat1, - &mut self.work.workmat2, - &mut self.work.workmat3, + &self.data.Rinv, + &mut self.data.workmat1, + &mut self.data.workmat2, + &mut self.data.workmat3, ) } } @@ -385,9 +389,9 @@ where { fn circ_op(&mut self, x: &mut [T], y: &[T], z: &[T]) { let (Y, Z, X) = ( - &mut self.work.workmat1, - &mut self.work.workmat2, - &mut self.work.workmat3, + &mut self.data.workmat1, + &mut self.data.workmat2, + &mut self.data.workmat3, ); _svec_to_mat(Y, y); _svec_to_mat(Z, z); diff --git a/src/solver/core/cones/socone.rs b/src/solver/core/cones/socone.rs index 2c1ba7f9..2c1c6f6b 100644 --- a/src/solver/core/cones/socone.rs +++ b/src/solver/core/cones/socone.rs @@ -8,7 +8,7 @@ use crate::{ // Second order Cone // ------------------------------------- -pub struct SecondOrderCone { +pub struct SecondOrderCone { dim: usize, //internal working variables for W and its products w: Vec, @@ -57,6 +57,10 @@ where true } + fn allows_primal_dual_scaling(&self) -> bool { + true + } + fn rectify_equilibration(&self, δ: &mut [T], e: &[T]) -> bool { δ.copy_from(e).recip().scale(e.mean()); @@ -204,7 +208,7 @@ where (αz, αs) } - fn compute_barrier(&self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { + fn compute_barrier(&mut self, z: &[T], s: &[T], dz: &[T], ds: &[T], α: T) -> T { let res_s = _soc_residual_shifted(s, ds, α); let res_z = _soc_residual_shifted(z, dz, α); @@ -393,6 +397,7 @@ where // fcn. The operation λ = Wz produces a borrow conflict // otherwise because λ is part of the cone's internal data // and we can't borrow self and &mut λ at the same time. +// Could also have been done using std::mem::take #[allow(non_snake_case)] fn _soc_mul_W_inner(y: &mut [T], x: &[T], α: T, β: T, w: &[T], η: T) diff --git a/src/solver/core/cones/supportedcone.rs b/src/solver/core/cones/supportedcone.rs index 33c03d23..139f6b21 100644 --- a/src/solver/core/cones/supportedcone.rs +++ b/src/solver/core/cones/supportedcone.rs @@ -11,7 +11,7 @@ use crate::algebra::triangular_number; /// API type describing the type of a conic constraint. /// -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub enum SupportedConeT { /// The zero cone (used for equality constraints). /// @@ -33,6 +33,11 @@ pub enum SupportedConeT { /// /// The parameter indicates the power. PowerConeT(T), + /// The generalized power cone. + /// + /// The parameter indicates the power and dimensions. + GenPowerConeT(Vec, usize), + /// The positive semidefinite cone in triangular form. /// /// The parameter indicates the matrix dimension, i.e. size = n @@ -55,6 +60,7 @@ impl SupportedConeT { SupportedConeT::PowerConeT(_) => 3, #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(dim) => triangular_number(*dim), + SupportedConeT::GenPowerConeT(α, dim2) => α.len() + *dim2, } } } @@ -72,15 +78,18 @@ where // for the constraint types, and then map them through // make_cone to get the internal cone representations. -pub fn make_cone(cone: SupportedConeT) -> SupportedCone { +pub fn make_cone(cone: &SupportedConeT) -> SupportedCone { match cone { - SupportedConeT::NonnegativeConeT(dim) => NonnegativeCone::::new(dim).into(), - SupportedConeT::ZeroConeT(dim) => ZeroCone::::new(dim).into(), - SupportedConeT::SecondOrderConeT(dim) => SecondOrderCone::::new(dim).into(), + SupportedConeT::NonnegativeConeT(dim) => NonnegativeCone::::new(*dim).into(), + SupportedConeT::ZeroConeT(dim) => ZeroCone::::new(*dim).into(), + SupportedConeT::SecondOrderConeT(dim) => SecondOrderCone::::new(*dim).into(), SupportedConeT::ExponentialConeT() => ExponentialCone::::new().into(), - SupportedConeT::PowerConeT(α) => PowerCone::::new(α).into(), + SupportedConeT::PowerConeT(α) => PowerCone::::new(*α).into(), + SupportedConeT::GenPowerConeT(α, dim2) => { + GenPowerCone::::new((*α).clone(), *dim2).into() + } #[cfg(feature = "sdp")] - SupportedConeT::PSDTriangleConeT(dim) => PSDTriangleCone::::new(dim).into(), + SupportedConeT::PSDTriangleConeT(dim) => PSDTriangleCone::::new(*dim).into(), } } @@ -101,16 +110,13 @@ where SecondOrderCone(SecondOrderCone), ExponentialCone(ExponentialCone), PowerCone(PowerCone), + GenPowerCone(GenPowerCone), #[cfg(feature = "sdp")] PSDTriangleCone(PSDTriangleCone), } -// we put PSDTriangleCone in a Box above since it is by the -// largest enum variant. We need some auto dereferencing -// for it so that it behaves like the other variants - // ------------------------------------- -// Finally, we need a tagging enum with no data fields to act +// we need a tagging enum with no data fields to act // as a bridge between the SupportedConeT API types and the // internal SupportedCone enum_dispatch wrapper. This enum // has no data attached at all, so we can just convert to a u8. @@ -129,6 +135,7 @@ pub(crate) enum SupportedConeTag { SecondOrderCone, ExponentialCone, PowerCone, + GenPowerCone, #[cfg(feature = "sdp")] PSDTriangleCone, } @@ -148,6 +155,7 @@ impl SupportedConeAsTag for SupportedConeT { SupportedConeT::PowerConeT(_) => SupportedConeTag::PowerCone, #[cfg(feature = "sdp")] SupportedConeT::PSDTriangleConeT(_) => SupportedConeTag::PSDTriangleCone, + SupportedConeT::GenPowerConeT(_, _) => SupportedConeTag::GenPowerCone, } } } @@ -163,6 +171,7 @@ impl SupportedConeAsTag for SupportedCone { SupportedCone::PowerCone(_) => SupportedConeTag::PowerCone, #[cfg(feature = "sdp")] SupportedCone::PSDTriangleCone(_) => SupportedConeTag::PSDTriangleCone, + SupportedCone::GenPowerCone(_) => SupportedConeTag::GenPowerCone, } } } @@ -178,6 +187,7 @@ impl SupportedConeTag { SupportedConeTag::PowerCone => "PowerCone", #[cfg(feature = "sdp")] SupportedConeTag::PSDTriangleCone => "PSDTriangleCone", + SupportedConeTag::GenPowerCone => "GenPowerCone", } } } diff --git a/src/solver/core/cones/zerocone.rs b/src/solver/core/cones/zerocone.rs index 6dc2eceb..9f4cca01 100644 --- a/src/solver/core/cones/zerocone.rs +++ b/src/solver/core/cones/zerocone.rs @@ -9,7 +9,7 @@ use core::marker::PhantomData; // Zero Cone // ------------------------------------- -pub struct ZeroCone { +pub struct ZeroCone { dim: usize, phantom: PhantomData, } @@ -42,6 +42,10 @@ where true } + fn allows_primal_dual_scaling(&self) -> bool { + true + } + fn rectify_equilibration(&self, δ: &mut [T], _e: &[T]) -> bool { δ.set(T::one()); false @@ -121,7 +125,7 @@ where (αmax, αmax) } - fn compute_barrier(&self, _z: &[T], _s: &[T], _dz: &[T], _ds: &[T], _α: T) -> T { + fn compute_barrier(&mut self, _z: &[T], _s: &[T], _dz: &[T], _ds: &[T], _α: T) -> T { T::zero() } } diff --git a/src/solver/core/kktsolvers/direct/quasidef/datamap.rs b/src/solver/core/kktsolvers/direct/quasidef/datamap.rs deleted file mode 100644 index d976d22d..00000000 --- a/src/solver/core/kktsolvers/direct/quasidef/datamap.rs +++ /dev/null @@ -1,73 +0,0 @@ -#![allow(non_snake_case)] - -use crate::algebra::*; -use crate::solver::core::cones::*; - -use super::*; - -pub struct LDLDataMap { - pub P: Vec, - pub A: Vec, - pub Hsblocks: Vec, //indices of the lower RHS blocks (by cone) - pub SOC_u: Vec>, //off diag dense columns u - pub SOC_v: Vec>, //off diag dense columns v - pub SOC_D: Vec, //diag of just the sparse SOC expansion D - - // all of above terms should be disjoint and their union - // should cover all of the user data in the KKT matrix. Now - // we make two last redundant indices that will tell us where - // the whole diagonal is, including structural zeros. - pub diagP: Vec, - pub diag_full: Vec, -} - -impl LDLDataMap { - pub fn new( - Pmat: &CscMatrix, - Amat: &CscMatrix, - cones: &CompositeCone, - ) -> Self { - let (m, n) = (Amat.nrows(), Pmat.nrows()); - let P = vec![0; Pmat.nnz()]; - let A = vec![0; Amat.nnz()]; - - // the diagonal of the ULHS KKT block P. - // NB : we fill in structural zeros here even if the matrix - // P is empty (e.g. as in an LP), so we can have entries in - // index Pdiag that are not present in the index P - let diagP = vec![0; n]; - - // make an index for each of the Hs blocks for each cone - let Hsblocks = allocate_kkt_Hsblocks::(cones); - - // now do the SOC expansion pieces - let nsoc = cones.type_count(SupportedConeTag::SecondOrderCone); - let p = 2 * nsoc; - let SOC_D = vec![0; p]; - - let mut SOC_u = Vec::>::with_capacity(nsoc); - let mut SOC_v = Vec::>::with_capacity(nsoc); - - for cone in cones.iter() { - // `cone` here will be of our SupportedCone enum wrapper, so - // we see if we can extract a SecondOrderCone `soc` - if let SupportedCone::SecondOrderCone(soc) = cone { - SOC_u.push(vec![0; soc.numel()]); - SOC_v.push(vec![0; soc.numel()]); - } - } - - let diag_full = vec![0; m + n + p]; - - Self { - P, - A, - Hsblocks, - SOC_u, - SOC_v, - SOC_D, - diagP, - diag_full, - } - } -} diff --git a/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs b/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs new file mode 100644 index 00000000..b01fa864 --- /dev/null +++ b/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs @@ -0,0 +1,403 @@ +#![allow(non_snake_case)] + +use super::*; +use crate::solver::core::cones::*; +use enum_dispatch::*; + +#[enum_dispatch(SparseExpansionMapTrait)] +pub(crate) enum SparseExpansionMap { + SOCExpansionMap(SOCExpansionMap), + GenPowExpansionMap(GenPowExpansionMap), +} + +#[enum_dispatch(SparseExpansionConeTrait)] +pub(crate) enum SparseExpansionCone<'a, T> +where + T: FloatT, +{ + SecondOrderCone(&'a SecondOrderCone), + GenPowerCone(&'a GenPowerCone), +} + +impl<'a, T> SupportedCone +where + T: FloatT, +{ + pub(crate) fn to_sparse(&'a self) -> Option> { + match self { + SupportedCone::SecondOrderCone(sc) => Some(SparseExpansionCone::SecondOrderCone(sc)), + SupportedCone::GenPowerCone(sc) => Some(SparseExpansionCone::GenPowerCone(sc)), + _ => None, + } + } + fn is_sparsifiable(&self) -> bool { + self.to_sparse().is_some() + } +} + +#[enum_dispatch] +pub(crate) trait SparseExpansionMapTrait { + fn pdim(&self) -> usize; + fn nnz_vec(&self) -> usize; + fn Dsigns(&self) -> &[i8]; +} + +impl SparseExpansionMapTrait for Vec { + fn pdim(&self) -> usize { + self.iter().fold(0, |pdim, map| pdim + map.pdim()) + } + fn nnz_vec(&self) -> usize { + self.iter().fold(0, |nnz, map| nnz + map.nnz_vec()) + } + fn Dsigns(&self) -> &[i8] { + unreachable!() + } +} + +type UpdateFcn = fn(&mut BoxedDirectLDLSolver, &mut CscMatrix, &[usize], &[T]) -> (); +type ScaleFcn = fn(&mut BoxedDirectLDLSolver, &mut CscMatrix, &[usize], T) -> (); +#[enum_dispatch] +pub(crate) trait SparseExpansionConeTrait +where + T: FloatT, +{ + fn expansion_map(&self) -> SparseExpansionMap; + fn csc_colcount_sparsecone( + &self, + map: &SparseExpansionMap, + K: &mut CscMatrix, + row: usize, + col: usize, + shape: MatrixTriangle, + ); + fn csc_fill_sparsecone( + &self, + map: &mut SparseExpansionMap, + K: &mut CscMatrix, + row: usize, + col: usize, + shape: MatrixTriangle, + ); + fn csc_update_sparsecone( + &self, + map: &SparseExpansionMap, + ldl: &mut BoxedDirectLDLSolver, + K: &mut CscMatrix, + updateFcn: UpdateFcn, + scaleFcn: ScaleFcn, + ); +} + +macro_rules! impl_map_recover { + ($CONE:ident,$MAP:ident) => { + impl<'a, T> $CONE { + pub(crate) fn recover_map(&self, map: &'a SparseExpansionMap) -> &'a $MAP { + match map { + SparseExpansionMap::$MAP(map) => map, + _ => panic!(), + } + } + pub(crate) fn recover_map_mut(&self, map: &'a mut SparseExpansionMap) -> &'a mut $MAP { + match map { + SparseExpansionMap::$MAP(map) => map, + _ => panic!(), + } + } + } + }; +} + +//-------------------------------------- +// Second order cone data map +//-------------------------------------- + +pub(crate) struct SOCExpansionMap { + u: Vec, //off diag dense columns u + v: Vec, //off diag dense columns v + D: [usize; 2], //diag D +} + +impl SOCExpansionMap { + pub fn new(cone: &SecondOrderCone) -> Self { + let u = vec![0; cone.numel()]; + let v = vec![0; cone.numel()]; + let D = [0; 2]; + Self { u, v, D } + } +} + +impl SparseExpansionMapTrait for SOCExpansionMap { + fn pdim(&self) -> usize { + 2 + } + fn nnz_vec(&self) -> usize { + 2 * self.v.len() + } + fn Dsigns(&self) -> &[i8] { + &[-1, 1] + } +} + +impl_map_recover!(SecondOrderCone, SOCExpansionMap); + +impl<'a, T> SparseExpansionConeTrait for &'a SecondOrderCone +where + T: FloatT, +{ + fn expansion_map(&self) -> SparseExpansionMap { + SparseExpansionMap::SOCExpansionMap(SOCExpansionMap::new(self)) + } + + fn csc_colcount_sparsecone( + &self, + map: &SparseExpansionMap, + K: &mut CscMatrix, + row: usize, + col: usize, + shape: MatrixTriangle, + ) { + let map = self.recover_map(map); + let nvars = self.numel(); + + match shape { + MatrixTriangle::Triu => { + K.colcount_colvec(nvars, row, col); // u column + K.colcount_colvec(nvars, row, col + 1); // v column + } + MatrixTriangle::Tril => { + K.colcount_rowvec(nvars, col, row); // u row + K.colcount_rowvec(nvars, col + 1, row); // v row + } + } + K.colcount_diag(col, map.pdim()); + } + + fn csc_fill_sparsecone( + &self, + map: &mut SparseExpansionMap, + K: &mut CscMatrix, + row: usize, + col: usize, + shape: MatrixTriangle, + ) { + let map = self.recover_map_mut(map); + + // fill structural zeros for u and v columns for this cone + // note v is the first extra row/column, u is second + match shape { + MatrixTriangle::Triu => { + K.fill_colvec(&mut map.v, row, col); //u + K.fill_colvec(&mut map.u, row, col + 1); //v + } + MatrixTriangle::Tril => { + K.fill_rowvec(&mut map.v, col, row); //u + K.fill_rowvec(&mut map.u, col + 1, row); //v + } + } + let pdim = map.pdim(); + K.fill_diag(&mut map.D, col, pdim); + } + + fn csc_update_sparsecone( + &self, + map: &SparseExpansionMap, + ldl: &mut BoxedDirectLDLSolver, + K: &mut CscMatrix, + updateFcn: UpdateFcn, + scaleFcn: ScaleFcn, + ) { + let map = self.recover_map(map); + let η2 = self.η * self.η; + + // off diagonal columns (or rows) + updateFcn(ldl, K, &map.u, &self.u); + updateFcn(ldl, K, &map.v, &self.v); + scaleFcn(ldl, K, &map.u, -η2); + scaleFcn(ldl, K, &map.v, -η2); + + //set diagonal to η^2*(-1,1) in the extended rows/cols + updateFcn(ldl, K, &map.D, &[-η2, η2]); + } +} + +//-------------------------------------- +// Generalized power cone data map +//-------------------------------------- + +pub(crate) struct GenPowExpansionMap { + p: Vec, //off diag dense columns p + q: Vec, //off diag dense columns q + r: Vec, //off diag dense columns r + D: [usize; 3], //diag D +} + +impl GenPowExpansionMap { + pub fn new(cone: &GenPowerCone) -> Self { + let p = vec![0; cone.numel()]; + let q = vec![0; cone.dim1()]; + let r = vec![0; cone.dim2()]; + let D = [0; 3]; + Self { p, q, r, D } + } +} + +impl SparseExpansionMapTrait for GenPowExpansionMap { + fn pdim(&self) -> usize { + 3 + } + fn nnz_vec(&self) -> usize { + self.p.len() + self.q.len() + self.r.len() + } + fn Dsigns(&self) -> &[i8] { + &[-1, -1, 1] + } +} + +impl_map_recover!(GenPowerCone, GenPowExpansionMap); + +impl<'a, T> SparseExpansionConeTrait for &'a GenPowerCone +where + T: FloatT, +{ + fn expansion_map(&self) -> SparseExpansionMap { + SparseExpansionMap::GenPowExpansionMap(GenPowExpansionMap::new(self)) + } + + fn csc_colcount_sparsecone( + &self, + map: &SparseExpansionMap, + K: &mut CscMatrix, + row: usize, + col: usize, + shape: MatrixTriangle, + ) { + let map = self.recover_map(map); + let nvars = self.numel(); + let dim1 = self.dim1(); + let dim2 = self.dim2(); + + match shape { + MatrixTriangle::Triu => { + K.colcount_colvec(dim1, row, col); //q column + K.colcount_colvec(dim2, row + dim1, col + 1); //r column + K.colcount_colvec(nvars, row, col + 2); //p column + } + MatrixTriangle::Tril => { + K.colcount_rowvec(dim1, col, row); //q row + K.colcount_rowvec(dim2, col + 1, row + dim1); //r row + K.colcount_rowvec(nvars, col + 2, row); //p row + } + } + K.colcount_diag(col, map.pdim()); + } + + fn csc_fill_sparsecone( + &self, + map: &mut SparseExpansionMap, + K: &mut CscMatrix, + row: usize, + col: usize, + shape: MatrixTriangle, + ) { + let map = self.recover_map_mut(map); + let dim1 = self.dim1(); + + match shape { + MatrixTriangle::Triu => { + K.fill_colvec(&mut map.q, row, col); //q column + K.fill_colvec(&mut map.r, row + dim1, col + 1); //r column + K.fill_colvec(&mut map.p, row, col + 2); //p column + } + MatrixTriangle::Tril => { + K.fill_rowvec(&mut map.q, col, row); //q row + K.fill_rowvec(&mut map.r, col + 1, row + dim1); //r row + K.fill_rowvec(&mut map.p, col + 2, row); //p row + } + } + K.colcount_diag(col, map.pdim()); + } + + fn csc_update_sparsecone( + &self, + map: &SparseExpansionMap, + ldl: &mut BoxedDirectLDLSolver, + K: &mut CscMatrix, + updateFcn: UpdateFcn, + scaleFcn: ScaleFcn, + ) { + let map = self.recover_map(map); + let data = &self.data; + let sqrtμ = data.μ.sqrt(); + + //&off diagonal columns (or rows), distribute √μ to off-diagonal terms + updateFcn(ldl, K, &map.q, &data.q); + updateFcn(ldl, K, &map.r, &data.r); + updateFcn(ldl, K, &map.p, &data.p); + scaleFcn(ldl, K, &map.q, -sqrtμ); + scaleFcn(ldl, K, &map.r, -sqrtμ); + scaleFcn(ldl, K, &map.p, -sqrtμ); + + //&normalize diagonal terms to 1/-1 in the extended rows/cols + updateFcn(ldl, K, &map.D, &[-T::one(), -T::one(), T::one()]); + } +} + +//-------------------------------------- +// LDL Data Map +//-------------------------------------- + +pub(crate) struct LDLDataMap { + pub P: Vec, + pub A: Vec, + pub Hsblocks: Vec, //indices of the lower RHS blocks (by cone) + pub sparse_maps: Vec, //sparse cone expansion terms + + // all of above terms should be disjoint and their union + // should cover all of the user data in the KKT matrix. Now + // we make two last redundant indices that will tell us where + // the whole diagonal is, including structural zeros. + pub diagP: Vec, + pub diag_full: Vec, +} + +impl LDLDataMap { + pub fn new( + Pmat: &CscMatrix, + Amat: &CscMatrix, + cones: &CompositeCone, + ) -> Self { + let (m, n) = (Amat.nrows(), Pmat.nrows()); + let P = vec![0; Pmat.nnz()]; + let A = vec![0; Amat.nnz()]; + + // the diagonal of the ULHS KKT block P. + // NB : we fill in structural zeros here even if the matrix + // P is empty (e.g. as in an LP), so we can have entries in + // index Pdiag that are not present in the index P + let diagP = vec![0; n]; + + // make an index for each of the Hs blocks for each cone + let Hsblocks = allocate_kkt_Hsblocks::(cones); + + // now do the sparse cone expansion pieces + let nsparse = cones.iter().filter(|&c| c.is_sparsifiable()).count(); + let mut sparse_maps = Vec::with_capacity(nsparse); + + for cone in cones.iter() { + if let Some(sc) = cone.to_sparse() { + sparse_maps.push(sc.expansion_map()); + } + } + + let diag_full = vec![0; m + n + sparse_maps.pdim()]; + + Self { + P, + A, + Hsblocks, + sparse_maps, + diagP, + diag_full, + } + } +} diff --git a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs index a26ac5ca..b7d410db 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs @@ -13,7 +13,7 @@ use std::iter::zip; // We require Send here to allow pyo3 builds to share // solver objects between threads. -type BoxedDirectLDLSolver = Box + Send>; +pub(crate) type BoxedDirectLDLSolver = Box + Send>; pub struct DirectLDLKKTSolver { // problem dimensions @@ -62,9 +62,15 @@ where n: usize, settings: &CoreSettings, ) -> Self { - // solving in sparse format. Need this many - // extra variables for SOCs - let p = 2 * cones.type_count(SupportedConeTag::SecondOrderCone); + // get a constructor for the LDL solver we should use, + // and also the matrix shape it requires + let (kktshape, ldl_ctor) = _get_ldlsolver_config(settings); + + //construct a KKT matrix of the right shape + let (KKT, map) = assemble_kkt_matrix(P, A, cones, kktshape); + + //Need this many extra variables for sparse cones + let p = map.sparse_maps.pdim(); // LHS/RHS/work for iterative refinement let x = vec![T::zero(); n + m + p]; @@ -74,19 +80,12 @@ where // the expected signs of D in LDL let mut dsigns = vec![1_i8; n + m + p]; - _fill_signs(&mut dsigns, m, n, p); + _fill_signs(&mut dsigns, m, n, &map); // updates to the diagonal of KKT will be // assigned here before updating matrix entries let Hsblocks = allocate_kkt_Hsblocks::(cones); - // get a constructor for the LDL solver we should use, - // and also the matrix shape it requires - let (kktshape, ldl_ctor) = _get_ldlsolver_config(settings); - - //construct a KKT matrix of the right shape - let (KKT, map) = assemble_kkt_matrix(P, A, cones, kktshape); - let diagonal_regularizer = T::zero(); // now make the LDL linear solver engine @@ -125,31 +124,16 @@ where values.negate(); _update_values(&mut self.ldlsolver, &mut self.KKT, index, values); - // update the scaled u and v columns. - let mut cidx = 0; // which of the SOCs are we working on? + let mut sparse_map_iter = map.sparse_maps.iter(); + let ldl = &mut self.ldlsolver; + let KKT = &mut self.KKT; for cone in cones.iter() { - // `cone` here will be of our SupportedCone enum wrapper, so - // we can extract a SecondOrderCone `soc` - if let SupportedCone::SecondOrderCone(soc) = cone { - let η2 = T::powi(soc.η, 2); - - //off diagonal columns (or rows)s - let KKT = &mut self.KKT; - let ldlsolver = &mut self.ldlsolver; - - _update_values(ldlsolver, KKT, &map.SOC_u[cidx], &soc.u); - _update_values(ldlsolver, KKT, &map.SOC_v[cidx], &soc.v); - _scale_values(ldlsolver, KKT, &map.SOC_u[cidx], -η2); - _scale_values(ldlsolver, KKT, &map.SOC_v[cidx], -η2); - - //add η^2*(-1/1) to diagonal in the extended rows/cols - _update_values(ldlsolver, KKT, &[map.SOC_D[cidx * 2]], &[-η2; 1]); - _update_values(ldlsolver, KKT, &[map.SOC_D[cidx * 2 + 1]], &[η2; 1]); - - cidx += 1; - } //end match - } //end for + if let Some(sc) = cone.to_sparse() { + let thismap = sparse_map_iter.next().unwrap(); + sc.csc_update_sparsecone(thismap, ldl, KKT, _update_values, _scale_values); + } + } self.regularize_and_refactor(settings) } //end fn @@ -193,7 +177,7 @@ where // extra helper functions, not required for KKTSolver trait fn getlhs(&self, lhsx: Option<&mut [T]>, lhsz: Option<&mut [T]>) { let x = &self.x; - let (m, n, _p) = (self.m, self.n, self.p); + let (m, n) = (self.m, self.n); if let Some(v) = lhsx { v.copy_from(&x[0..n]); @@ -405,16 +389,17 @@ fn _scale_values_KKT(KKT: &mut CscMatrix, index: &[usize], scale: } } -fn _fill_signs(signs: &mut [i8], m: usize, n: usize, p: usize) { +fn _fill_signs(signs: &mut [i8], m: usize, n: usize, map: &LDLDataMap) { signs.fill(1); //flip expected negative signs of D in LDL signs[n..(n + m)].iter_mut().for_each(|x| *x = -*x); - //the trailing block of p entries should - //have alternating signs - signs[(n + m)..(n + m + p)] - .iter_mut() - .step_by(2) - .for_each(|x| *x = -*x); + let mut p = m + n; + // assign D signs for sparse expansion cones + for thismap in map.sparse_maps.iter() { + let thisp = thismap.pdim(); + signs[p..(p + thisp)].copy_from_slice(thismap.Dsigns()); + p += thisp; + } } diff --git a/src/solver/core/kktsolvers/direct/quasidef/utils.rs b/src/solver/core/kktsolvers/direct/quasidef/kkt_assembly.rs similarity index 52% rename from src/solver/core/kktsolvers/direct/quasidef/utils.rs rename to src/solver/core/kktsolvers/direct/quasidef/kkt_assembly.rs index 3f07b55c..dbd8d272 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/utils.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/kkt_assembly.rs @@ -1,11 +1,10 @@ #![allow(non_snake_case)] -use super::datamap::*; +use super::datamaps::*; use crate::algebra::*; use crate::solver::core::cones::CompositeCone; use crate::solver::core::cones::*; use num_traits::Zero; -use std::iter::zip; pub(crate) fn allocate_kkt_Hsblocks(cones: &CompositeCone) -> Vec where @@ -19,58 +18,46 @@ where vec![Z::zero(); nnz] } -pub fn assemble_kkt_matrix( +pub(crate) fn assemble_kkt_matrix( P: &CscMatrix, A: &CscMatrix, cones: &CompositeCone, shape: MatrixTriangle, ) -> (CscMatrix, LDLDataMap) { - let (m, n) = (A.nrows(), P.nrows()); - let n_socs = cones.type_count(SupportedConeTag::SecondOrderCone); - let p = 2 * n_socs; - - let mut maps = LDLDataMap::new(P, A, cones); + let mut map = LDLDataMap::new(P, A, cones); + let (m, n) = A.size(); + let p = map.sparse_maps.pdim(); // entries actually on the diagonal of P let nnz_diagP = P.count_diagonal_entries(); // total entries in the Hs blocks - let nnz_Hsblocks = maps.Hsblocks.len(); - - // entries in the dense columns u/v of the - // sparse SOC expansion terms. 2 is for - // counting elements in both columns - let nnz_SOC_vecs = 2 * maps.SOC_u.iter().fold(0, |acc, block| acc + block.len()); - - //entries in the sparse SOC diagonal extension block - let nnz_SOC_ext = maps.SOC_D.len(); + let nnz_Hsblocks = map.Hsblocks.len(); - let nnzKKT = P.nnz() + // Number of elements in P - n - // Number of elements in diagonal top left block - nnz_diagP + // remove double count on the diagonal if P has entries - A.nnz() + // Number of nonzeros in A - nnz_Hsblocks + // Number of elements in diagonal below A' - nnz_SOC_vecs + // Number of elements in sparse SOC off diagonal columns - nnz_SOC_ext; // Number of elements in diagonal of SOC extension + let nnzKKT = P.nnz() + // Number of elements in P + n - // Number of elements in diagonal top left block + nnz_diagP + // remove double count on the diagonal if P has entries + A.nnz() + // Number of nonzeros in A + nnz_Hsblocks + // Number of elements in diagonal below A' + map.sparse_maps.nnz_vec() + // Number of elements in sparse cone off diagonals + p; //Number of elements in diagonal of sparse cones - let Kdim = m + n + p; - let mut K = CscMatrix::::spalloc((Kdim, Kdim), nnzKKT); + let mut K = CscMatrix::::spalloc((m + n + p, m + n + p), nnzKKT); - _kkt_assemble_colcounts(&mut K, P, A, cones, (m, n, p), shape); - _kkt_assemble_fill(&mut K, &mut maps, P, A, cones, (m, n, p), shape); + _kkt_assemble_colcounts(&mut K, P, A, cones, &map, shape); + _kkt_assemble_fill(&mut K, P, A, cones, &mut map, shape); - (K, maps) + (K, map) } - fn _kkt_assemble_colcounts( K: &mut CscMatrix, P: &CscMatrix, A: &CscMatrix, cones: &CompositeCone, - mnp: (usize, usize, usize), + map: &LDLDataMap, shape: MatrixTriangle, ) { - let (m, n, p) = (mnp.0, mnp.1, mnp.2); + let (m, n) = A.size(); // use K.p to hold nnz entries in each // column of the KKT matrix @@ -89,121 +76,83 @@ fn _kkt_assemble_colcounts( } } - // add the Hs blocks in the lower right + // track the next sparse column to fill (assuming triu fill) + let mut pcol = m + n; //next sparse column to fill + let mut sparse_map_iter = map.sparse_maps.iter(); + for (i, cone) in cones.iter().enumerate() { - let firstcol = cones.rng_cones[i].start + n; + let row = cones.rng_cones[i].start + n; + + // add the Hs blocks in the lower right let blockdim = cone.numel(); if cone.Hs_is_diagonal() { - K.colcount_diag(firstcol, blockdim); + K.colcount_diag(row, blockdim); } else { - K.colcount_dense_triangle(firstcol, blockdim, shape); + K.colcount_dense_triangle(row, blockdim, shape); } - } - // count dense columns for each SOC - let mut socidx = 0; // which SOC are we working on? - - for (i, cone) in cones.iter().enumerate() { - if let SupportedCone::SecondOrderCone(SOC) = cone { - // we will add the u and v columns for this cone - let nvars = SOC.numel(); - let headidx = cones.rng_cones[i].start; - - // which column does u go into? - let col = m + n + 2 * socidx; - - match shape { - MatrixTriangle::Triu => { - K.colcount_colvec(nvars, headidx + n, col); // u column - K.colcount_colvec(nvars, headidx + n, col + 1); // v column - } - MatrixTriangle::Tril => { - K.colcount_rowvec(nvars, col, headidx + n); // u row - K.colcount_rowvec(nvars, col + 1, headidx + n); // v row - } - } - socidx += 1; + //add sparse expansions columns for sparse cones + if let Some(sc) = cone.to_sparse() { + let thismap = sparse_map_iter.next().unwrap(); + sc.csc_colcount_sparsecone(thismap, K, row, pcol, shape); + pcol += thismap.pdim(); } } - - // add diagonal block in the lower RH corner - // to allow for the diagonal terms in SOC expansion - K.colcount_diag(n + m, p); } fn _kkt_assemble_fill( K: &mut CscMatrix, - maps: &mut LDLDataMap, P: &CscMatrix, A: &CscMatrix, cones: &CompositeCone, - mnp: (usize, usize, usize), + map: &mut LDLDataMap, shape: MatrixTriangle, ) { - let (m, n, p) = (mnp.0, mnp.1, mnp.2); + let (m, n) = A.size(); // cumsum total entries to convert to K.p K.colcount_to_colptr(); match shape { MatrixTriangle::Triu => { - K.fill_block(P, &mut maps.P, 0, 0, MatrixShape::N); + K.fill_block(P, &mut map.P, 0, 0, MatrixShape::N); K.fill_missing_diag(P, 0); // after adding P, since triu form // fill in value for A, top right (transposed/rowwise) - K.fill_block(A, &mut maps.A, 0, n, MatrixShape::T); + K.fill_block(A, &mut map.A, 0, n, MatrixShape::T); } MatrixTriangle::Tril => { K.fill_missing_diag(P, 0); // before adding P, since tril form - K.fill_block(P, &mut maps.P, 0, 0, MatrixShape::T); + K.fill_block(P, &mut map.P, 0, 0, MatrixShape::T); // fill in value for A, bottom left (not transposed) - K.fill_block(A, &mut maps.A, n, 0, MatrixShape::N); + K.fill_block(A, &mut map.A, n, 0, MatrixShape::N); } } - // add the the Hs blocks in the lower right - for (i, (cone, rng_cone)) in zip(cones.iter(), &cones.rng_cones).enumerate() { - let firstcol = rng_cone.start + n; + // track the next sparse column to fill (assuming triu fill) + let mut pcol = m + n; //next sparse column to fill + let mut sparse_map_iter = map.sparse_maps.iter_mut(); + + for (i, cone) in cones.iter().enumerate() { + let row = cones.rng_cones[i].start + n; + + // add the Hs blocks in the lower right let blockdim = cone.numel(); - let block = &mut maps.Hsblocks[cones.rng_blocks[i].clone()]; + let block = &mut map.Hsblocks[cones.rng_blocks[i].clone()]; + if cone.Hs_is_diagonal() { - K.fill_diag(block, firstcol, blockdim); + K.fill_diag(block, row, blockdim); } else { - K.fill_dense_triangle(block, firstcol, blockdim, shape); + K.fill_dense_triangle(block, row, blockdim, shape); } - } - - // fill in dense columns for each SOC - let mut socidx = 0; //which SOC are we working on? - for (i, cone) in cones.iter().enumerate() { - if let SupportedCone::SecondOrderCone(_) = cone { - let headidx = cones.rng_cones[i].start; - - // which column does u go into (if triu)? - let col = m + n + 2 * socidx; - - // fill structural zeros for u and v columns for this cone - // note v is the first extra row/column, u is second - match shape { - MatrixTriangle::Triu => { - K.fill_colvec(&mut maps.SOC_v[socidx], headidx + n, col); //u - K.fill_colvec(&mut maps.SOC_u[socidx], headidx + n, col + 1); - //v - } - MatrixTriangle::Tril => { - K.fill_rowvec(&mut maps.SOC_v[socidx], col, headidx + n); //u - K.fill_rowvec(&mut maps.SOC_u[socidx], col + 1, headidx + n); - //v - } - } - - socidx += 1; + //add sparse expansions columns for sparse cones + if let Some(sc) = cone.to_sparse() { + let thismap = sparse_map_iter.next().unwrap(); + sc.csc_fill_sparsecone(thismap, K, row, pcol, shape); + pcol += thismap.pdim(); } } - // fill in SOC diagonal extension with diagonal of structural zeros - K.fill_diag(&mut maps.SOC_D, n + m, p); - // backshift the colptrs to recover K.p again K.backshift_colptrs(); @@ -213,19 +162,19 @@ fn _kkt_assemble_fill( match shape { MatrixTriangle::Triu => { // matrix is triu, so diagonal is last in each column - maps.diag_full.copy_from_slice(&K.colptr[1..]); - maps.diag_full.iter_mut().for_each(|x| *x -= 1); + map.diag_full.copy_from_slice(&K.colptr[1..]); + map.diag_full.iter_mut().for_each(|x| *x -= 1); // and the diagonal of just the upper left - maps.diagP.copy_from_slice(&K.colptr[1..=n]); - maps.diagP.iter_mut().for_each(|x| *x -= 1); + map.diagP.copy_from_slice(&K.colptr[1..=n]); + map.diagP.iter_mut().for_each(|x| *x -= 1); } MatrixTriangle::Tril => { // matrix is tril, so diagonal is first in each column - maps.diag_full + map.diag_full .copy_from_slice(&K.colptr[0..K.colptr.len() - 1]); // and the diagonal of just the upper left - maps.diagP.copy_from_slice(&K.colptr[0..n]); + map.diagP.copy_from_slice(&K.colptr[0..n]); } } } diff --git a/src/solver/core/kktsolvers/direct/quasidef/mod.rs b/src/solver/core/kktsolvers/direct/quasidef/mod.rs index 5cef5e0d..917d809e 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/mod.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/mod.rs @@ -4,12 +4,12 @@ use crate::algebra::*; pub mod ldlsolvers; //flatten direct KKT module structure -mod datamap; +mod datamaps; mod directldlkktsolver; -mod utils; -pub use datamap::*; +mod kkt_assembly; +use datamaps::*; pub use directldlkktsolver::*; -pub use utils::*; +use kkt_assembly::*; pub trait DirectLDLSolver { fn update_values(&mut self, index: &[usize], values: &[T]); diff --git a/src/solver/core/solver.rs b/src/solver/core/solver.rs index 36c826ce..bd222e40 100644 --- a/src/solver/core/solver.rs +++ b/src/solver/core/solver.rs @@ -196,7 +196,10 @@ where // main loop // ---------- - let mut scaling = ScalingStrategy::PrimalDual; + let mut scaling = { + if self.cones.allows_primal_dual_scaling() {ScalingStrategy::PrimalDual} + else {ScalingStrategy::Dual} + }; loop { @@ -386,7 +389,7 @@ mod internal { -> T; /// backtrack a step direction to the barrier - fn backtrack_step_to_barrier(&self, αinit: T) -> T; + fn backtrack_step_to_barrier(&mut self, αinit: T) -> T; /// Scaling strategy checkpointing functions fn strategy_checkpoint_insufficient_progress( @@ -472,16 +475,16 @@ mod internal { α } - fn backtrack_step_to_barrier(&self, αinit: T) -> T { - let backtrack = self.settings.core().linesearch_backtrack_step; + fn backtrack_step_to_barrier(&mut self, αinit: T) -> T { + let step = self.settings.core().linesearch_backtrack_step; let mut α = αinit; for _ in 0..50 { - let barrier = self.variables.barrier(&self.step_lhs, α, &self.cones); + let barrier = self.variables.barrier(&self.step_lhs, α, &mut self.cones); if barrier < T::one() { return α; } else { - α = backtrack * α; // backtrack line search + α = step * α; } } α diff --git a/src/solver/core/traits.rs b/src/solver/core/traits.rs index f7bdeee4..3131867a 100644 --- a/src/solver/core/traits.rs +++ b/src/solver/core/traits.rs @@ -79,13 +79,13 @@ pub trait Variables { /// Overwrite values with those from another object fn copy_from(&mut self, src: &Self); - /// Apply NT scaling to the a collection of cones. + /// Apply NT scaling to a collection of cones. fn scale_cones(&self, cones: &mut Self::C, μ: T, scaling_strategy: ScalingStrategy) -> bool; /// Compute the barrier function - fn barrier(&self, step: &Self, α: T, cones: &Self::C) -> T; + fn barrier(&self, step: &Self, α: T, cones: &mut Self::C) -> T; /// Rescale variables, e.g. to renormalize iterates /// in a homogeneous embedding diff --git a/src/solver/implementations/default/info_print.rs b/src/solver/implementations/default/info_print.rs index f2089bc1..5cebcc00 100644 --- a/src/solver/implementations/default/info_print.rs +++ b/src/solver/implementations/default/info_print.rs @@ -58,6 +58,7 @@ where _print_conedims_by_type(cones, SupportedConeTag::SecondOrderCone); _print_conedims_by_type(cones, SupportedConeTag::ExponentialCone); _print_conedims_by_type(cones, SupportedConeTag::PowerCone); + _print_conedims_by_type(cones, SupportedConeTag::GenPowerCone); #[cfg(feature = "sdp")] _print_conedims_by_type(cones, SupportedConeTag::PSDTriangleCone); @@ -209,14 +210,15 @@ fn _get_precision_string() -> String { fn _print_conedims_by_type(cones: &CompositeCone, conetag: SupportedConeTag) { let maxlistlen = 5; + let count = cones.get_type_count(conetag); + //skip if there are none of this type - if !cones.type_counts.contains_key(&conetag) { + if count == 0 { return; } // how many of this type of cone? let name = conetag.as_str(); - let count = cones.type_counts[&conetag]; // drops trailing "Cone" part of name let name = &name[0..name.len() - 4]; diff --git a/src/solver/implementations/default/kktsystem.rs b/src/solver/implementations/default/kktsystem.rs index 1c5b4032..e9d0a24e 100644 --- a/src/solver/implementations/default/kktsystem.rs +++ b/src/solver/implementations/default/kktsystem.rs @@ -204,24 +204,27 @@ where data: &DefaultProblemData, settings: &DefaultSettings, ) -> bool { - let is_success; + let mut is_success; if data.P.nnz() == 0 { // LP initialization - // solve with [0;b] as a RHS to get (x,-s) initializers // zero out any sparse cone variables at end self.workx.fill(T::zero()); self.workz.copy_from(&data.b); self.kktsolver.setrhs(&self.workx, &self.workz); - self.kktsolver.solve( + is_success = self.kktsolver.solve( Some(&mut variables.x), Some(&mut variables.s), settings.core(), ); variables.s.negate(); - // solve with [-c;0] as a RHS to get z initializer + if !is_success { + return is_success; + } + + // solve with [-q;0] as a RHS to get z initializer // zero out any sparse cone variables at end self.workx.axpby(-T::one(), &data.q, T::zero()); self.workz.fill(T::zero()); @@ -232,8 +235,7 @@ where .solve(None, Some(&mut variables.z), settings.core()); } else { //QP initialization - self.workx.copy_from(&data.q); - self.workx.negate(); + self.workx.scalarop_from(|q| -q, &data.q); self.workz.copy_from(&data.b); self.kktsolver.setrhs(&self.workx, &self.workz); is_success = self.kktsolver.solve( @@ -241,8 +243,7 @@ where Some(&mut variables.z), settings.core(), ); - variables.s.copy_from(&variables.z); - variables.s.negate(); + variables.s.scalarop_from(|z| -z, &variables.z); } is_success } diff --git a/src/solver/implementations/default/variables.rs b/src/solver/implementations/default/variables.rs index 55f9d1ea..117e731b 100644 --- a/src/solver/implementations/default/variables.rs +++ b/src/solver/implementations/default/variables.rs @@ -193,7 +193,7 @@ where cones.update_scaling(&self.s, &self.z, μ, scaling_strategy) } - fn barrier(&self, step: &Self, α: T, cones: &CompositeCone) -> T { + fn barrier(&self, step: &Self, α: T, cones: &mut CompositeCone) -> T { let central_coef = (cones.degree() + 1).as_T(); let cur_τ = self.τ + α * step.τ; diff --git a/tests/basic_genpowcone.rs b/tests/basic_genpowcone.rs new file mode 100644 index 00000000..38921d26 --- /dev/null +++ b/tests/basic_genpowcone.rs @@ -0,0 +1,55 @@ +#![allow(non_snake_case)] + +use clarabel::{algebra::*, solver::*}; + +#[test] +fn test_powcone() { + // solve the following power cone problem + // max x1^0.6 y^0.4 + x2^0.1 + // s.t. x1, y, x2 >= 0 + // x1 + 2y + 3x2 == 3 + // which is equivalent to + // max z1 + z2 + // s.t. (x1, y, z1) in K_pow(0.6) + // (x2, 1, z2) in K_pow(0.1) + // x1 + 2y + 3x2 == 3 + + // x = (x1, y, z1, x2, y2, z2) + let n = 6; + let P = CscMatrix::::zeros((n, n)); + let c = vec![0., 0., -1., 0., 0., -1.]; + + // (x1, y, z1) in K_pow(0.6) + // (x2, y2, z2) in K_pow(0.1) + let mut A1 = CscMatrix::::identity(n); + A1.negate(); + let b1 = vec![0.; n]; + let cones1 = vec![ + GenPowerConeT(vec![0.6, 0.4], 1), + GenPowerConeT(vec![0.1, 0.9], 1), + ]; + + // x1 + 2y + 3x2 == 3 + // y2 == 1 + let A2 = CscMatrix::from(&[ + [1., 2., 0., 3., 0., 0.], // + [0., 0., 0., 0., 1., 0.], // + ]); + + let b2 = vec![3., 1.]; + let cones2 = vec![ZeroConeT(2)]; + + let A = CscMatrix::vcat(&A1, &A2); + let b = [b1, b2].concat(); + let cones = [cones1, cones2].concat(); + + let settings = DefaultSettings::default(); + let mut solver = DefaultSolver::new(&P, &c, &A, &b, &cones, settings); + + solver.solve(); + + assert_eq!(solver.solution.status, SolverStatus::Solved); + + let refobj = -1.8458; + assert!(f64::abs(solver.info.cost_primal - refobj) <= 1e-3); +} diff --git a/tests/basic_powcone.rs b/tests/basic_powcone.rs index 414c5507..f1818a7f 100644 --- a/tests/basic_powcone.rs +++ b/tests/basic_powcone.rs @@ -3,7 +3,7 @@ use clarabel::{algebra::*, solver::*}; #[test] -fn test_powcone_feasible() { +fn test_powcone() { // solve the following power cone problem // max x1^0.6 y^0.4 + x2^0.1 // s.t. x1, y, x2 >= 0 @@ -15,6 +15,7 @@ fn test_powcone_feasible() { // x1 + 2y + 3x2 == 3 // x = (x1, y, z1, x2, y2, z2) + let n = 6; let P = CscMatrix::::zeros((n, n)); let c = vec![0., 0., -1., 0., 0., -1.]; @@ -28,13 +29,10 @@ fn test_powcone_feasible() { // x1 + 2y + 3x2 == 3 // y2 == 1 - let A2 = CscMatrix::new( - 2, // m - 6, // n - vec![0, 1, 2, 2, 3, 4, 4], //colptr - vec![0, 0, 0, 1], //rowval - vec![1., 2., 3., 1.], //nzval - ); + let A2 = CscMatrix::from(&[ + [1., 2., 0., 3., 0., 0.], // + [0., 0., 0., 0., 1., 0.], // + ]); let b2 = vec![3., 1.]; let cones2 = vec![ZeroConeT(2)]; diff --git a/tests/basic_sdp.rs b/tests/basic_sdp.rs index 7c6766af..815ce3ab 100644 --- a/tests/basic_sdp.rs +++ b/tests/basic_sdp.rs @@ -90,7 +90,7 @@ fn test_sdp_primal_infeasible() { A2.negate(); let A = CscMatrix::vcat(&A, &A2); b.extend(vec![0.0; b.len()]); - cones.extend([cones[0]]); + cones.extend([cones[0].clone()]); let settings = DefaultSettings::default(); From e56ba48cee1968ccbe9f148b61f927e6f5993e3b Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Sun, 17 Sep 2023 20:25:01 +0100 Subject: [PATCH 40/52] remove alloc from solution finalize --- src/solver/core/cones/socone.rs | 2 +- .../implementations/default/solution.rs | 20 +++++++------------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/solver/core/cones/socone.rs b/src/solver/core/cones/socone.rs index 786decda..16b58f75 100644 --- a/src/solver/core/cones/socone.rs +++ b/src/solver/core/cones/socone.rs @@ -275,7 +275,7 @@ where let λ1ds1 = self.λ[1..].dot(&ds[1..]); let w1ds1 = self.w[1..].dot(&ds[1..]); - out.scalarop_from(|zi| -zi, &z); + out.scalarop_from(|zi| -zi, z); out[0] = z[0]; let c = self.λ[0] * ds[0] - λ1ds1; diff --git a/src/solver/implementations/default/solution.rs b/src/solver/implementations/default/solution.rs index 3f81ac70..cefef4fd 100644 --- a/src/solver/implementations/default/solution.rs +++ b/src/solver/implementations/default/solution.rs @@ -3,6 +3,7 @@ use crate::{ algebra::*, solver::core::{traits::Solution, SolverStatus}, }; +use itertools::izip; use std::iter::zip; /// Standard-form solver type implementing the [`Solution`](crate::solver::core::traits::Solution) trait @@ -78,20 +79,13 @@ where self.x.copy_from(&variables.x).hadamard(d).scale(scaleinv); if let Some(map) = data.presolver.reduce_map.as_ref() { - //PJG : temporary alloc makes implementation much easier - //here. could also use something like e or einv as scratch - let mut tmp = vec![T::zero(); variables.s.len()]; + // - tmp.copy_from(&variables.z) - .hadamard(e) - .scale(scaleinv / cscale); - for (vi, mapi) in zip(&tmp, &map.keep_index) { - self.z[*mapi] = *vi; - } - - tmp.copy_from(&variables.s).hadamard(einv).scale(scaleinv); - for (vi, mapi) in zip(&tmp, &map.keep_index) { - self.s[*mapi] = *vi; + for (&zi, &si, &ei, &einvi, &mapi) in + izip!(&variables.z, &variables.s, e, einv, &map.keep_index) + { + self.z[mapi] = zi * ei * (scaleinv / cscale); + self.s[mapi] = si * einvi * scaleinv; } // eliminated constraints get huge slacks From 21ed369c60f8e2e33f78987146212461795e4187 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Sun, 17 Sep 2023 22:18:25 +0100 Subject: [PATCH 41/52] fix unit tests --- src/solver/core/kktsolvers/direct/quasidef/datamaps.rs | 3 ++- .../core/kktsolvers/direct/quasidef/directldlkktsolver.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs b/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs index bb9f6845..fbc606d5 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/datamaps.rs @@ -314,7 +314,8 @@ where K.fill_rowvec(&mut map.p, col + 2, row); //p row } } - K.colcount_diag(col, map.pdim()); + let pdim = map.pdim(); + K.fill_diag(&mut map.D, col, pdim); } fn csc_update_sparsecone( diff --git a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs index 57b88951..18611b36 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/directldlkktsolver.rs @@ -137,7 +137,7 @@ where } self.regularize_and_refactor(settings) - } //end fn + } fn setrhs(&mut self, rhsx: &[T], rhsz: &[T]) { let (m, n, p) = (self.m, self.n, self.p); From 50e56223aee717d9554b8f57bac49cb7fb162fe2 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Mon, 18 Sep 2023 17:29:51 +0100 Subject: [PATCH 42/52] genpowcone support for julia wrapper --- src/julia/ClarabelRs/src/interface.jl | 112 ++++++++++++++------------ src/julia/ClarabelRs/src/types.jl | 27 ++++++- src/julia/interface.rs | 44 ++++------ src/julia/types.rs | 21 ++++- 4 files changed, 119 insertions(+), 85 deletions(-) diff --git a/src/julia/ClarabelRs/src/interface.jl b/src/julia/ClarabelRs/src/interface.jl index c17c20e2..d9bb1463 100644 --- a/src/julia/ClarabelRs/src/interface.jl +++ b/src/julia/ClarabelRs/src/interface.jl @@ -43,7 +43,7 @@ function solver_new_jlrs(P,q,A,b,cones,settings) # first flatten the cones to three primitive arrays - (cone_enums, cone_ints, cone_floats) = ccall_cones_to_arrays(cones) + (cone_data) = ccall_cones_to_array(cones) ptr = ccall(Libdl.dlsym(librust,:solver_new_jlrs),Ptr{Cvoid}, ( @@ -51,18 +51,14 @@ function solver_new_jlrs(P,q,A,b,cones,settings) Ref{VectorJLRS{Float64}}, #q Ref{CscMatrixJLRS}, #A Ref{VectorJLRS{Float64}}, #b - Ref{VectorJLRS{UInt8}}, #cone_enums - Ref{VectorJLRS{UInt64}}, #cone_ints - Ref{VectorJLRS{Float64}}, #cone_floats + Ref{VectorJLRS{ConeDataJLRS}}, #cone_data in tagged form Cstring #json_settings ), CscMatrixJLRS(P), #P VectorJLRS(q), #q CscMatrixJLRS(A), #A VectorJLRS(b), #b - VectorJLRS(cone_enums), #cone_enums - VectorJLRS(cone_ints), #cone_ints - VectorJLRS(cone_floats), #cone_floats + VectorJLRS(cone_data), #cone data in tagged form serialize(settings), #serialized settings ) @@ -103,54 +99,64 @@ end # functions for passing cones and settings # ------------------------------------- -# it is not obvious at all how to pass data through -# ccall for a data-carrying enum type in rust. This -# makes it very difficult to pass the `cones` object -# directly. Here we make an enum for the different -# cone types, with a complementary enum type on -# the rust side with equivalent base types and values -# We will pass three arrays to rust : the enum value, -# an integer (for dimensions) and a float (for powers) -# Every cone needs at least of one these values. -# Values not needed for a particular cone get a zero -# placeholder - -function ccall_cones_to_arrays(cones::Vector{Clarabel.SupportedCone}) - - cone_enums = zeros(UInt8,length(cones)) - cone_ints = zeros(UInt64,length(cones)) - cone_floats = zeros(Float64,length(cones)) - - for (i,cone) in enumerate(cones) - - if isa(cone, Clarabel.ZeroConeT) - cone_enums[i] = UInt8(ZeroConeT::ConeEnumJLRS) - cone_ints[i] = cone.dim; - - elseif isa(cone, Clarabel.NonnegativeConeT) - cone_enums[i] = UInt8(NonnegativeConeT::ConeEnumJLRS) - cone_ints[i] = cone.dim; - - elseif isa(cone, Clarabel.SecondOrderConeT) - cone_enums[i] = UInt8(SecondOrderConeT::ConeEnumJLRS) - cone_ints[i] = cone.dim; - - elseif isa(cone, Clarabel.ExponentialConeT) - cone_enums[i] = UInt8(ExponentialConeT::ConeEnumJLRS) - - elseif isa(cone, Clarabel.PowerConeT) - cone_enums[i] = UInt8(PowerConeT::ConeEnumJLRS) - cone_floats[i] = cone.α - - elseif isa(cone, Clarabel.PSDTriangleConeT) - cone_enums[i] = UInt8(PSDTriangleConeT::ConeEnumJLRS) - cone_ints[i] = cone.dim; - else - error("Cone type ", typeof(cone), " is not supported through this interface."); - end + +function ccall_cones_to_array(cones::Vector{Clarabel.SupportedCone}) + + rscones = ConeDataJLRS[] + sizehint!(rscones,length(cones)) + + for cone in cones + + rscone = begin + if isa(cone, Clarabel.ZeroConeT) + ConeDataJLRS(ZeroConeT::ConeEnumJLRS; + int = cone.dim, + ) + + elseif isa(cone, Clarabel.NonnegativeConeT) + ConeDataJLRS( + NonnegativeConeT::ConeEnumJLRS; + int = cone.dim, + ) + + elseif isa(cone, Clarabel.SecondOrderConeT) + ConeDataJLRS( + SecondOrderConeT::ConeEnumJLRS; + int = cone.dim, + ) + + elseif isa(cone, Clarabel.ExponentialConeT) + ConeDataJLRS( + ExponentialConeT::ConeEnumJLRS + ) + + elseif isa(cone, Clarabel.PowerConeT) + ConeDataJLRS( + PowerConeT::ConeEnumJLRS; + float = cone.α + ) + + elseif isa(cone, Clarabel.GenPowerConeT) + ConeDataJLRS( + GenPowerConeT::ConeEnumJLRS; + int = cone.dim2, + vec = cone.α + ) + + elseif isa(cone, Clarabel.PSDTriangleConeT) + ConeDataJLRS( + PSDTriangleConeT::ConeEnumJLRS; + int = cone.dim, + ) + else + error("Cone type ", typeof(cone), " is not supported through this interface."); + end + end + + push!(rscones,rscone) end - return (cone_enums, cone_ints, cone_floats) + return rscones end diff --git a/src/julia/ClarabelRs/src/types.jl b/src/julia/ClarabelRs/src/types.jl index ca31713e..f27b8387 100644 --- a/src/julia/ClarabelRs/src/types.jl +++ b/src/julia/ClarabelRs/src/types.jl @@ -1,7 +1,7 @@ # The types defined here are for exchanging data # between Rust and Julia. -struct VectorJLRS{T<:Real} +struct VectorJLRS{T} p::Ptr{T} len::UInt64 @@ -17,10 +17,30 @@ function Vector(v::VectorJLRS{T}) where {T} unsafe_wrap(Vector{T},v.p,v.len) end +# it is not obvious at all how to pass data through +# ccall for a data-carrying enum type in rust. This +# makes it very difficult to pass the `cones` object +# directly. Here we make an enum for the different +# cone types, with a complementary enum type on +# the rust side with equivalent base types and values +# We then pass a data structure that looks something +# like a tagged union #NB: mutability here causes alignment errors? -struct CscMatrixJLRS + struct ConeDataJLRS + tag::UInt8 + int::UInt64 + float::Float64 + vec::VectorJLRS{Float64} + function ConeDataJLRS(enum; int = 0,float = 0.0,vec = Float64[]) + return new(UInt8(enum),int,float,VectorJLRS(vec)) + end +end + + +#NB: mutability here causes alignment errors? +struct CscMatrixJLRS m::UInt64 n::UInt64 colptr::VectorJLRS{Int64} @@ -76,7 +96,8 @@ end SecondOrderConeT = 2 ExponentialConeT = 3 PowerConeT = 4 - PSDTriangleConeT = 5 + GenPowerConeT = 5 + PSDTriangleConeT = 6 end diff --git a/src/julia/interface.rs b/src/julia/interface.rs index 6ac88330..2c4a2d2a 100644 --- a/src/julia/interface.rs +++ b/src/julia/interface.rs @@ -22,32 +22,24 @@ fn from_ptr(ptr: *mut c_void) -> Box> { unsafe { Box::from_raw(ptr as *mut DefaultSolver) } } -// function for receiving cone specifications in rust -// in a flattened form from Julia - -fn ccall_arrays_to_cones( - cones_enums: &VectorJLRS, - cones_ints: &VectorJLRS, - cones_floats: &VectorJLRS, -) -> Vec> { - let mut cones: Vec> = Vec::new(); - - assert_eq!(cones_enums.len(), cones_ints.len()); - assert_eq!(cones_enums.len(), cones_floats.len()); +// function for sending cone specifications into rust +// from a tagged union type form supplied from Julia - // convert to rust vector types from raw pointers - let cones_enums = Vec::from(cones_enums); - let cones_ints = Vec::from(cones_ints); - let _cones_floats = Vec::from(cones_floats); +fn ccall_arrays_to_cones(jlcones: &VectorJLRS) -> Vec> { + let mut cones: Vec> = Vec::new(); - for i in 0..cones_enums.len() { - let cone = match FromPrimitive::from_u8(cones_enums[i]) { - Some(ConeEnumJLRS::ZeroConeT) => ZeroConeT(cones_ints[i] as usize), - Some(ConeEnumJLRS::NonnegativeConeT) => NonnegativeConeT(cones_ints[i] as usize), - Some(ConeEnumJLRS::SecondOrderConeT) => SecondOrderConeT(cones_ints[i] as usize), + for jlcone in jlcones.to_slice() { + let cone = match FromPrimitive::from_u8(jlcone.tag) { + Some(ConeEnumJLRS::ZeroConeT) => ZeroConeT(jlcone.int), + Some(ConeEnumJLRS::NonnegativeConeT) => NonnegativeConeT(jlcone.int), + Some(ConeEnumJLRS::SecondOrderConeT) => SecondOrderConeT(jlcone.int), Some(ConeEnumJLRS::ExponentialConeT) => ExponentialConeT(), - Some(ConeEnumJLRS::PowerConeT) => PowerConeT(_cones_floats[i]), - Some(ConeEnumJLRS::PSDTriangleConeT) => PSDTriangleConeT(cones_ints[i] as usize), + Some(ConeEnumJLRS::PowerConeT) => PowerConeT(jlcone.float), + Some(ConeEnumJLRS::GenPowerConeT) => { + let alpha = Vec::::from(&jlcone.vec); + GenPowerConeT(alpha, jlcone.int) + } + Some(ConeEnumJLRS::PSDTriangleConeT) => PSDTriangleConeT(jlcone.int), None => panic!("Received unrecognized cone type"), }; cones.push(cone) @@ -61,9 +53,7 @@ pub(crate) extern "C" fn solver_new_jlrs( q: &VectorJLRS, A: &CscMatrixJLRS, b: &VectorJLRS, - cones_enums: &VectorJLRS, - cones_ints: &VectorJLRS, - cones_floats: &VectorJLRS, + jlcones: &VectorJLRS, json_settings: *const std::os::raw::c_char, ) -> *mut c_void { let P = P.to_CscMatrix(); @@ -71,7 +61,7 @@ pub(crate) extern "C" fn solver_new_jlrs( let q = Vec::from(q); let b = Vec::from(b); - let cones = ccall_arrays_to_cones(cones_enums, cones_ints, cones_floats); + let cones = ccall_arrays_to_cones(jlcones); let settings = settings_from_json(json_settings); diff --git a/src/julia/types.rs b/src/julia/types.rs index 111faee3..ece4d05a 100644 --- a/src/julia/types.rs +++ b/src/julia/types.rs @@ -8,13 +8,22 @@ use std::slice; // The types defined here are for exchanging data // between Rust and Julia. +#[derive(Debug, Clone)] #[repr(C)] -#[derive(Debug)] pub(crate) struct VectorJLRS { pub p: *const T, pub len: libc::size_t, } +#[derive(Debug, Clone)] +#[repr(C)] +pub(crate) struct ConeDataJLRS { + pub tag: u8, + pub int: usize, + pub float: f64, + pub vec: VectorJLRS, +} + impl VectorJLRS where T: std::clone::Clone + std::fmt::Debug, @@ -24,11 +33,15 @@ where unsafe { slice::from_raw_parts(self.p, self.len) } } + #[allow(dead_code)] pub(crate) fn len(&self) -> usize { self.len } } +// The following conversions are used to convert from +// Julia problem data into Rust types. + impl From<&VectorJLRS> for Vec { fn from(v: &VectorJLRS) -> Self { v.to_slice().iter().map(|&e| e as usize).collect() @@ -45,6 +58,9 @@ where } } +// The following conversions are used to convert from Rust data +// back to Julia. Here we only need vectors of floats. + impl From<&Vec> for VectorJLRS where T: std::clone::Clone + std::fmt::Debug, @@ -124,5 +140,6 @@ pub(crate) enum ConeEnumJLRS { SecondOrderConeT = 2, ExponentialConeT = 3, PowerConeT = 4, - PSDTriangleConeT = 5, + GenPowerConeT = 5, + PSDTriangleConeT = 6, } From 5a6bcfeaeaf05fece2b88b8e4310c0018172f301 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Mon, 18 Sep 2023 22:19:57 +0100 Subject: [PATCH 43/52] genpowcone support for python wrapper --- src/python/cones_py.rs | 48 +++++++++++++++++++++++++++++++------- src/python/cscmatrix_py.rs | 5 ++++ src/python/module_py.rs | 1 + src/python/pyblas/mod.rs | 4 ++-- 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/python/cones_py.rs b/src/python/cones_py.rs index 5b340a6e..9c1c0027 100644 --- a/src/python/cones_py.rs +++ b/src/python/cones_py.rs @@ -2,7 +2,6 @@ #![allow(clippy::new_without_default)] use crate::solver::core::{cones::SupportedConeT, cones::SupportedConeT::*}; -use core::ops::Deref; use pyo3::{exceptions::PyTypeError, prelude::*}; use std::fmt::Write; @@ -29,6 +28,14 @@ fn __repr__cone__float(name: &str, pow: f64) -> String { s } +// Python display functionality for genpowercone objects +// with floating point vector parameters +fn __repr__genpowcone(name: &str, alpha: &[f64], dim2: usize) -> String { + let mut s = String::new(); + write!(s, "{}[\n α = {:?},\n dim2 = {}\n]", name, alpha, dim2).unwrap(); + s +} + #[pyclass(name = "ZeroConeT")] pub struct PyZeroConeT { #[pyo3(get)] @@ -106,6 +113,24 @@ impl PyPowerConeT { } } +#[pyclass(name = "GenPowerConeT")] +pub struct PyGenPowerConeT { + #[pyo3(get)] + pub α: Vec, + #[pyo3(get)] + pub dim2: usize, +} +#[pymethods] +impl PyGenPowerConeT { + #[new] + pub fn new(α: Vec, dim2: usize) -> Self { + Self { α, dim2 } + } + pub fn __repr__(&self) -> String { + __repr__genpowcone("GenPowerConeT", &self.α, self.dim2) + } +} + #[pyclass(name = "PSDTriangleConeT")] pub struct PyPSDTriangleConeT { #[pyo3(get)] @@ -129,10 +154,9 @@ impl PyPSDTriangleConeT { #[derive(Debug)] pub struct PySupportedCone(SupportedConeT); -impl Deref for PySupportedCone { - type Target = SupportedConeT; - fn deref(&self) -> &Self::Target { - &self.0 +impl From for SupportedConeT { + fn from(cone: PySupportedCone) -> Self { + cone.0 } } @@ -158,6 +182,11 @@ impl<'a> FromPyObject<'a> for PySupportedCone { let α: f64 = obj.getattr("α")?.extract()?; Ok(PySupportedCone(PowerConeT(α))) } + "GenPowerConeT" => { + let α: Vec = obj.getattr("α")?.extract()?; + let dim2: usize = obj.getattr("dim2")?.extract()?; + Ok(PySupportedCone(GenPowerConeT(α, dim2))) + } "PSDTriangleConeT" => { let dim: usize = obj.getattr("dim")?.extract()?; Ok(PySupportedCone(PSDTriangleConeT(dim))) @@ -173,7 +202,10 @@ impl<'a> FromPyObject<'a> for PySupportedCone { pub(crate) fn _py_to_native_cones(cones: Vec) -> Vec> { //force a vector of PySupportedCone back into a vector - //of rust native SupportedCone. The Py cone is just - //a wrapper; deref gives us the native object. - cones.iter().map(|x| *x.deref()).collect() + //of rust native SupportedCone. + let mut out = Vec::with_capacity(cones.len()); + for cone in cones { + out.push(cone.into()); + } + out } diff --git a/src/python/cscmatrix_py.rs b/src/python/cscmatrix_py.rs index eb2f29af..f2233106 100644 --- a/src/python/cscmatrix_py.rs +++ b/src/python/cscmatrix_py.rs @@ -17,6 +17,11 @@ impl Deref for PyCscMatrix { &self.0 } } +impl From for CscMatrix { + fn from(mat: PyCscMatrix) -> Self { + mat.0 + } +} impl<'a> FromPyObject<'a> for PyCscMatrix { fn extract(obj: &'a PyAny) -> PyResult { diff --git a/src/python/module_py.rs b/src/python/module_py.rs index 0208a831..a0fcc626 100644 --- a/src/python/module_py.rs +++ b/src/python/module_py.rs @@ -46,6 +46,7 @@ fn clarabel(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; //other API data types diff --git a/src/python/pyblas/mod.rs b/src/python/pyblas/mod.rs index 5d591598..4d3f27f0 100644 --- a/src/python/pyblas/mod.rs +++ b/src/python/pyblas/mod.rs @@ -19,8 +19,8 @@ mod lapack_types; // initialization of the python module to ensure that lazy_statics // are already realised before making an FFI call to blas/lapack. pub fn force_load() { - let _ = blas_wrappers::force_load(); - let _ = lapack_wrappers::force_load(); + blas_wrappers::force_load(); + lapack_wrappers::force_load(); } // utilities for scipy blas/lapack import From 2c8a4ebf0c7ce2d6954e0cfbd71b601db64dfba0 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Tue, 19 Sep 2023 20:21:52 +0100 Subject: [PATCH 44/52] doc update --- src/solver/core/cones/supportedcone.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/solver/core/cones/supportedcone.rs b/src/solver/core/cones/supportedcone.rs index 139f6b21..e502e9e3 100644 --- a/src/solver/core/cones/supportedcone.rs +++ b/src/solver/core/cones/supportedcone.rs @@ -35,7 +35,10 @@ pub enum SupportedConeT { PowerConeT(T), /// The generalized power cone. /// - /// The parameter indicates the power and dimensions. + /// The first vector of parameters supplies the nonnegative powers "alpha" of + /// the left-hand side of the constraint. The second scalar parameter provides + /// the dimension of the 2-norm bounded vector in the right-hand side of the + /// constraint. The "alpha" terms must sum to 1. GenPowerConeT(Vec, usize), /// The positive semidefinite cone in triangular form. From 5d452672c72dd10e8326beb1b2a8734520a187b8 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Wed, 20 Sep 2023 07:26:37 +0100 Subject: [PATCH 45/52] qdldl error handling --- src/qdldl/qdldl.rs | 115 ++++++++++-------- src/qdldl/test.rs | 83 ++++++++++--- .../direct/quasidef/ldlsolvers/qdldl.rs | 5 +- 3 files changed, 134 insertions(+), 69 deletions(-) diff --git a/src/qdldl/qdldl.rs b/src/qdldl/qdldl.rs index 9a2c29d4..ad99ce41 100644 --- a/src/qdldl/qdldl.rs +++ b/src/qdldl/qdldl.rs @@ -3,10 +3,25 @@ use crate::algebra::*; use core::cmp::{max, min}; use derive_builder::Builder; use std::iter::zip; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum QDLDLError { + #[error("Matrix dimension fields are incompatible")] + IncompatibleDimension, + #[error("Matrix has a zero column")] + EmptyColumn, + #[error("Matrix is not upper triangular")] + NotUpperTriangular, + #[error("Matrix factorization produced a zero pivot")] + ZeroPivot, + #[error("Invalid permutation vector")] + InvalidPermutation, +} /// Required settings for [`QDLDLFactorisation`](QDLDLFactorisation) -#[derive(Builder, Debug)] +#[derive(Builder, Debug, Clone)] pub struct QDLDLSettings { #[builder(default = "1.0")] amd_dense_scale: f64, @@ -57,7 +72,12 @@ impl QDLDLFactorisation where T: FloatT, { - pub fn new(Ain: &CscMatrix, opts: Option>) -> QDLDLFactorisation { + pub fn new( + Ain: &CscMatrix, + opts: Option>, + ) -> Result, QDLDLError> { + //sanity check on structure + check_structure(Ain)?; _qdldl_new(Ain, opts) } @@ -72,9 +92,7 @@ where // Solves in place (x replaces b) pub fn solve(&mut self, b: &mut [T]) { // bomb if logical factorisation only - if self.is_logical { - panic!("Can't solve with logical factorisation only"); - } + assert!(self.is_logical); // bomb if b is the wrong size assert_eq!(b.len(), self.D.len()); @@ -126,7 +144,7 @@ where } } - pub fn refactor(&mut self) { + pub fn refactor(&mut self) -> Result<(), QDLDLError> { // It never makes sense to call refactor for a logical // factorization since it will always be the same. Calling // this function implies that we want a numerical factorization @@ -137,15 +155,31 @@ where &mut self.Dinv, &mut self.workspace, self.is_logical, - ); + ) + } +} + +fn check_structure(A: &CscMatrix) -> Result<(), QDLDLError> { + if !A.is_square() { + return Err(QDLDLError::IncompatibleDimension); + } + + if !A.is_triu() { + return Err(QDLDLError::NotUpperTriangular); } + + //Error if A doesn't have at least one entry in every column + if !A.colptr.windows(2).all(|c| c[0] < c[1]) { + return Err(QDLDLError::EmptyColumn); + } + + Ok(()) } fn _qdldl_new( Ain: &CscMatrix, opts: Option>, -) -> QDLDLFactorisation { - assert!(Ain.is_square()); +) -> Result, QDLDLError> { let n = Ain.nrows(); //get default values if no options passed at all @@ -156,7 +190,7 @@ fn _qdldl_new( //user would need to pass (0..n).collect() explicitly let (perm, iperm); if let Some(_perm) = opts.perm { - iperm = _invperm(&_perm); + iperm = _invperm(&_perm)?; perm = _perm; } else { (perm, iperm) = _get_amd_ordering(Ain, opts.amd_dense_scale); @@ -183,7 +217,7 @@ fn _qdldl_new( opts.regularize_enable, opts.regularize_eps, opts.regularize_delta, - ); + )?; //total nonzeros in factorization let sumLnz = workspace.Lnz.iter().sum(); @@ -196,9 +230,9 @@ fn _qdldl_new( let mut Dinv = vec![T::zero(); n]; // factor the matrix into A = LDL^T - _factor(&mut L, &mut D, &mut Dinv, &mut workspace, opts.logical); + _factor(&mut L, &mut D, &mut Dinv, &mut workspace, opts.logical)?; - QDLDLFactorisation { + Ok(QDLDLFactorisation { perm, iperm, L, @@ -206,7 +240,7 @@ fn _qdldl_new( Dinv, workspace, is_logical: opts.logical, - } + }) } #[derive(Debug)] @@ -253,7 +287,7 @@ where regularize_enable: bool, regularize_eps: T, regularize_delta: T, - ) -> Self { + ) -> Result { let mut etree = vec![0; triuA.ncols()]; let mut Lnz = vec![0; triuA.ncols()]; //nonzeros in each L column let mut iwork = vec![0; triuA.ncols() * 3]; @@ -268,8 +302,7 @@ where &mut iwork, &mut Lnz, &mut etree, - ) - .unwrap(); + )?; // positive inertia count. let positive_inertia = 0; @@ -277,7 +310,7 @@ where // number of regularized entries in D. None to start let regularize_count = 0; - Self { + Ok(Self { etree, Lnz, iwork, @@ -291,7 +324,7 @@ where regularize_eps, regularize_delta, regularize_count, - } + }) } } @@ -301,7 +334,7 @@ fn _factor( Dinv: &mut [T], workspace: &mut QDLDLWorkspace, logical: bool, -) { +) -> Result<(), QDLDLError> { if logical { L.nzval.fill(T::zero()); D.fill(T::zero()); @@ -332,12 +365,11 @@ fn _factor( workspace.regularize_eps, workspace.regularize_delta, &mut workspace.regularize_count, - ); + )?; - if pos_d_count.is_err() { - panic!("Zero entry in D (matrix is not quasidefinite)"); - } - workspace.positive_inertia = pos_d_count.unwrap(); + workspace.positive_inertia = pos_d_count; + + Ok(()) } const QDLDL_UNKNOWN: usize = usize::MAX; @@ -354,27 +386,18 @@ fn _etree( work: &mut [usize], Lnz: &mut [usize], etree: &mut [usize], -) -> Result { +) -> Result { // zero out Lnz and work. Set all etree values to unknown work.fill(0); Lnz.fill(0); etree.fill(QDLDL_UNKNOWN); - //Abort if A doesn't have at least one entry in every column - if !Ap.windows(2).all(|c| c[0] < c[1]) { - return Err(-1); - } - // compute the elimination tree for j in 0..n { work[j] = j; for istart in Ai.iter().take(Ap[j + 1]).skip(Ap[j]) { let mut i = *istart; - if i > j { - return Err(-1); - } - while work[i] != j { if etree[i] == QDLDL_UNKNOWN { etree[i] = j; @@ -413,7 +436,7 @@ fn _factor_inner( regularize_eps: T, regularize_delta: T, regularize_count: &mut usize, -) -> Result { +) -> Result { *regularize_count = 0; let mut positiveValuesInD = 0; @@ -451,7 +474,7 @@ fn _factor_inner( } if D[0] == T::zero() { - return Err(-1); + return Err(QDLDLError::ZeroPivot); } if D[0] > T::zero() { positiveValuesInD += 1; @@ -580,7 +603,7 @@ fn _factor_inner( // in D. If we hit a zero, we can't factor // this matrix, so abort if D[k] == T::zero() { - return Err(-1); + return Err(QDLDLError::ZeroPivot); } if D[k] > T::zero() { positiveValuesInD += 1; @@ -674,17 +697,17 @@ fn _solve(Lp: &[usize], Li: &[usize], Lx: &[T], Dinv: &[T], b: &mut [ } // Construct an inverse permutation from a permutation -fn _invperm(p: &[usize]) -> Vec { +fn _invperm(p: &[usize]) -> Result, QDLDLError> { let mut b = vec![0; p.len()]; for (i, j) in p.iter().enumerate() { if *j < p.len() && b[*j] == 0 { b[*j] = i; } else { - panic!("Input vector is not a permutation"); + return Err(QDLDLError::InvalidPermutation); } } - b + Ok(b) } // internal permutation and inverse permutation @@ -703,15 +726,7 @@ fn _ipermute(x: &mut [T], b: &[T], p: &[usize]) { // inverse permutation vector `iperm`." fn _permute_symmetric(A: &CscMatrix, iperm: &[usize]) -> (CscMatrix, Vec) { // perform a number of argument checks - let (m, n) = A.size(); - if m != n { - panic!("Matrix A must be sparse and square") - }; - - if n != iperm.len() { - panic!("Dimensions of sparse matrix A must equal the length of iperm"); - } - + let (_m, n) = A.size(); let mut P = CscMatrix::::spalloc((n, n), A.nnz()); // we will record a mapping of entries from A to PAPt diff --git a/src/qdldl/test.rs b/src/qdldl/test.rs index 80e5f87f..d712c3cf 100644 --- a/src/qdldl/test.rs +++ b/src/qdldl/test.rs @@ -32,23 +32,20 @@ fn inf_norm_diff(a: &[T], b: &[T]) -> T { #[test] fn test_invperm() { let perm = vec![3, 0, 2, 1]; - let iperm = _invperm(&perm); - assert_eq!(iperm, vec![1, 3, 2, 0]); + assert!(_invperm(&perm).is_ok()) } //test fail on bad permutation #[test] -#[should_panic] -fn test_invperm_bad_perm_panic1() { +fn test_invperm_bad_perm1() { let perm = vec![3, 0, 2, 0]; //repeated index - _invperm(&perm); + assert!(_invperm(&perm).is_err()) } #[test] -#[should_panic] -fn test_invperm_bad_perm_panic2() { +fn test_invperm_bad_perm2() { let perm = vec![4, 0, 2, 1]; //index too big - _invperm(&perm); + assert!(_invperm(&perm).is_err()) } #[test] @@ -160,7 +157,7 @@ fn test_permute_symmetric() { } let perm: Vec = vec![2, 3, 0, 1]; - let iperm = _invperm(&perm); + let iperm = _invperm(&perm).unwrap(); let (P, _) = _permute_symmetric(&A, &iperm); assert_eq!(&P.colptr, &vec![0, 1, 3, 5, 8]); @@ -206,7 +203,7 @@ fn test_solve_basic() { .build() .unwrap(); - let mut factors = QDLDLFactorisation::new(&A, Some(opts)); + let mut factors = QDLDLFactorisation::new(&A, Some(opts)).unwrap(); let x = [1., -2., 3., -4.]; let mut b = [20.0, -22.0, 32.0, -7.0]; //solves in place @@ -214,7 +211,7 @@ fn test_solve_basic() { assert!(inf_norm_diff(&x, &b) <= 1e-8); //now with all defaults, including amd - let mut factors = QDLDLFactorisation::new(&A, None); + let mut factors = QDLDLFactorisation::new(&A, None).unwrap(); let x = [1., -2., 3., -4.]; let mut b = [20.0, -22.0, 32.0, -7.0]; //solves in place @@ -226,7 +223,7 @@ fn test_solve_basic() { .perm(vec![3, 0, 2, 1]) .build() .unwrap(); - let mut factors = QDLDLFactorisation::new(&A, Some(opts)); + let mut factors = QDLDLFactorisation::new(&A, Some(opts)).unwrap(); let x = [1., -2., 3., -4.]; let mut b = [20.0, -22.0, 32.0, -7.0]; //solves in place @@ -244,10 +241,10 @@ fn test_solve_logical() { .build() .unwrap(); - let mut factors = QDLDLFactorisation::new(&A, Some(opts)); + let mut factors = QDLDLFactorisation::new(&A, Some(opts)).unwrap(); let mut b = [20.0, -22.0, 32.0, -7.0]; //solves in place - factors.solve(&mut b); + factors.solve(&mut b); //should panic } #[test] @@ -259,11 +256,65 @@ fn test_solve_logical_refactor() { .build() .unwrap(); - let mut factors = QDLDLFactorisation::new(&A, Some(opts)); + let mut factors = QDLDLFactorisation::new(&A, Some(opts)).unwrap(); let x = [1., -2., 3., -4.]; let mut b = [20.0, -22.0, 32.0, -7.0]; //solves in place - factors.refactor(); + assert!(factors.refactor().is_ok()); factors.solve(&mut b); assert!(inf_norm_diff(&x, &b) <= 1e-8); } + +#[test] +fn test_bad_numeric_pivot() { + //Disable regularization to force an exact zero pivot + let opts = QDLDLSettingsBuilder::default() + .regularize_enable(false) + .build() + .unwrap(); + + //set the first element of A to zero (top left) + let mut A = test_matrix_4x4(); + A.nzval[0] = 0.; + assert!(QDLDLFactorisation::new(&A, Some(opts.clone())).is_err()); + + //set the final element of A to zero (top left) + let mut A = test_matrix_4x4(); + *A.nzval.last_mut().unwrap() = 0.; + assert!(QDLDLFactorisation::new(&A, Some(opts)).is_err()); +} + +#[test] +fn test_lower_triangular() { + //create a matrix with a logical zero on the diagonal + let opts = QDLDLSettingsBuilder::default() + .logical(true) + .build() + .unwrap(); + + let A = CscMatrix::from(&[ + // + [1.0, 3.0, 5.0], + [2.0, 3.0, 6.0], + [1.0, 4.0, 7.0], + ]); + assert!(QDLDLFactorisation::new(&A, Some(opts)).is_err()); +} + +#[test] +fn test_zero_column_error() { + //create a matrix with a logical zero on the diagonal + let opts = QDLDLSettingsBuilder::default() + .logical(true) + .build() + .unwrap(); + + let A = CscMatrix::from(&[ + // + [1.0, 0.0, 5.0], + [0.0, 0.0, 6.0], + [1.0, 0.0, 7.0], + ]); + + assert!(QDLDLFactorisation::new(&A, Some(opts)).is_err()); +} diff --git a/src/solver/core/kktsolvers/direct/quasidef/ldlsolvers/qdldl.rs b/src/solver/core/kktsolvers/direct/quasidef/ldlsolvers/qdldl.rs index 0b220282..32d7de78 100644 --- a/src/solver/core/kktsolvers/direct/quasidef/ldlsolvers/qdldl.rs +++ b/src/solver/core/kktsolvers/direct/quasidef/ldlsolvers/qdldl.rs @@ -37,7 +37,7 @@ where .build() .unwrap(); - let factors = QDLDLFactorisation::::new(KKT, Some(opts)); + let factors = QDLDLFactorisation::::new(KKT, Some(opts)).unwrap(); Self { factors } } @@ -71,8 +71,7 @@ where //QDLDL has maintained its own version of the permuted //KKT matrix through custom update/scale/offset methods, //so we ignore the KKT matrix provided by the caller - self.factors.refactor(); - + self.factors.refactor().unwrap(); self.factors.Dinv.is_finite() } From eac08910941c562fd693204ebab30e6b10a13258 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Wed, 20 Sep 2023 08:28:47 +0100 Subject: [PATCH 46/52] update qdldl docstrings --- src/qdldl/qdldl.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/qdldl/qdldl.rs b/src/qdldl/qdldl.rs index ad99ce41..1173b819 100644 --- a/src/qdldl/qdldl.rs +++ b/src/qdldl/qdldl.rs @@ -5,6 +5,8 @@ use derive_builder::Builder; use std::iter::zip; use thiserror::Error; +/// Error codes returnable from [`QDLDLFactorisation`](QDLDLFactorisation) factor operations + #[derive(Error, Debug)] pub enum QDLDLError { #[error("Matrix dimension fields are incompatible")] @@ -48,7 +50,7 @@ where } } -/// Performs $LDL^T$ factorization of a symmetric quasidefinite matrix. +/// Performs $LDL^T$ factorization of a symmetric quasidefinite matrix #[derive(Debug)] pub struct QDLDLFactorisation { From f4e426009ab30c99232b36f277e26c6f54c0f943 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Wed, 20 Sep 2023 08:41:06 +0100 Subject: [PATCH 47/52] fix bad assert --- src/qdldl/qdldl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qdldl/qdldl.rs b/src/qdldl/qdldl.rs index 1173b819..5a26dc7c 100644 --- a/src/qdldl/qdldl.rs +++ b/src/qdldl/qdldl.rs @@ -94,7 +94,7 @@ where // Solves in place (x replaces b) pub fn solve(&mut self, b: &mut [T]) { // bomb if logical factorisation only - assert!(self.is_logical); + assert!(!self.is_logical); // bomb if b is the wrong size assert_eq!(b.len(), self.D.len()); From b2f97c44970d06d96205565db1a69ab9d98f646c Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Wed, 20 Sep 2023 08:53:49 +0100 Subject: [PATCH 48/52] bump version --- CHANGELOG.md | 1 + Cargo.toml | 2 +- README.md | 2 +- src/julia/ClarabelRs/Project.toml | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 061afb30..1cdf228f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,7 @@ offline against the Julia-based benchmark problem suite, but this will not appea - Ported all documentation to the common site [here](https://github.com/oxfordcontrol/ClarabelDocs) +[0.6.0]: https://github.com/oxfordcontrol/Clarabel.rs/compare/v0.5.1...v0.6.0 [0.5.1]: https://github.com/oxfordcontrol/Clarabel.rs/compare/v0.5.0...v0.5.1 [0.5.0]: https://github.com/oxfordcontrol/Clarabel.rs/compare/v0.4.1...v0.5.0 [0.4.1]: https://github.com/oxfordcontrol/Clarabel.rs/compare/v0.4.0...v0.4.1 diff --git a/Cargo.toml b/Cargo.toml index da8d152c..f1d65fb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "clarabel" -version = "0.5.1" +version = "0.6.0" authors = ["Paul Goulart "] edition = "2021" rust-version = "1.60" diff --git a/README.md b/README.md index 638684fb..ae2e7c98 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Interior Point Conic Optimization for Rust and Python - +

diff --git a/src/julia/ClarabelRs/Project.toml b/src/julia/ClarabelRs/Project.toml index f06d1bb7..1f03e8df 100644 --- a/src/julia/ClarabelRs/Project.toml +++ b/src/julia/ClarabelRs/Project.toml @@ -1,7 +1,7 @@ name = "ClarabelRs" uuid = "a0c58a0a-712c-48b7-9fd4-64369ecb2011" authors = ["Paul Goulart "] -version = "0.5.1" +version = "0.6.0" [deps] Clarabel = "61c947e1-3e6d-4ee4-985a-eec8c727bd6e" @@ -11,4 +11,4 @@ Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [compat] -Clarabel = "0.5.1" +Clarabel = "0.6.0" From 052414d91c4f61ac8653a2f74d25545d685eff05 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Wed, 20 Sep 2023 09:23:46 +0100 Subject: [PATCH 49/52] bump version --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cdf228f..daf4ccef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Version numbering in this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). We aim to keep the core solver functionality and minor releases in sync between the Rust/Python and Julia implementations. Small fixes that affect one implementation only may result in the patch release versions differing. + +## [0.6.0] - 2023-02-06 +### Changed + +This version introduces support for the generalized power cone and implements stability and speed improvements for SOC problems. SOCs with +dimension less than or equal to 4 are now treated as special cases with dense Hessian blocks. + +- Introduces support for the generalized power cone and implements stability and speed improvements for SOC problems. +- SOCs with dimension less than or equal to 4 are now treated as special cases with dense Hessian blocks. +- Fixes bad initialization point for non-quadratic objectives +- Improved convergence speed for QPs with no constraints or only ZeroCone constraints. +- Internal code restructuring for cones with sparsifiable Hessian blocks. + +### Rust specific changes +- Added additional documentation and utilities [#43](https://github.com/oxfordcontrol/Clarabel.rs/issues/43),[#46](https://github.com/oxfordcontrol/Clarabel.rs/issues/46). +- Allow printing of internal timers through Julia wrappers in ClarabelRs [#44](https://github.com/oxfordcontrol/Clarabel.rs/issues/44) +- Updated keywords for crates.io [#45](https://github.com/oxfordcontrol/Clarabel.rs/issues/45) +- Better error reporting from internal QDLDL factor methods. Fixes [#49](https://github.com/oxfordcontrol/Clarabel.rs/issues/49) + + ## [0.5.1] - 2023-02-06 ### Changed Fixes convergence edge case in KKT direct solve iterative refinement. From 62a2af60e4310f8a5cdc5a97315dcfb5b5ad4ef6 Mon Sep 17 00:00:00 2001 From: Paul Goulart Date: Wed, 20 Sep 2023 09:27:52 +0100 Subject: [PATCH 50/52] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daf4ccef..22bb57be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,8 @@ Version numbering in this project adheres to [Semantic Versioning](https://semve This version introduces support for the generalized power cone and implements stability and speed improvements for SOC problems. SOCs with dimension less than or equal to 4 are now treated as special cases with dense Hessian blocks. -- Introduces support for the generalized power cone and implements stability and speed improvements for SOC problems. -- SOCs with dimension less than or equal to 4 are now treated as special cases with dense Hessian blocks. +- Introduces support for the generalized power cone. +- Implements stability and speed improvements for SOC problems. SOCs with dimension less than or equal to 4 are now treated as special cases with dense Hessian blocks. - Fixes bad initialization point for non-quadratic objectives - Improved convergence speed for QPs with no constraints or only ZeroCone constraints. - Internal code restructuring for cones with sparsifiable Hessian blocks. From ba6ee8f1c1d84dbc670501fa6f72ab5288df4e58 Mon Sep 17 00:00:00 2001 From: Paul Goulart Date: Wed, 20 Sep 2023 09:28:40 +0100 Subject: [PATCH 51/52] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22bb57be..1ddf904c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Version numbering in this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). We aim to keep the core solver functionality and minor releases in sync between the Rust/Python and Julia implementations. Small fixes that affect one implementation only may result in the patch release versions differing. -## [0.6.0] - 2023-02-06 +## [0.6.0] - 2023-20-09 ### Changed This version introduces support for the generalized power cone and implements stability and speed improvements for SOC problems. SOCs with From 2b794ece4a980daab74de14eb4c0e3b60af992f7 Mon Sep 17 00:00:00 2001 From: goulart-paul Date: Wed, 20 Sep 2023 10:49:25 +0100 Subject: [PATCH 52/52] update feature list --- README.md | 2 +- src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ae2e7c98..d3dce369 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Clarabel is also available in a Julia implementation. See [here](https://github ## Features -* __Versatile__: Clarabel.rs solves linear programs (LPs), quadratic programs (QPs), second-order cone programs (SOCPs) and semidefinite programs (SDPs). It also solves problems with exponential and power cone constraints. +* __Versatile__: Clarabel.rs solves linear programs (LPs), quadratic programs (QPs), second-order cone programs (SOCPs) and semidefinite programs (SDPs). It also solves problems with exponential, power cone and generalized power cone constraints. * __Quadratic objectives__: Unlike interior point solvers based on the standard homogeneous self-dual embedding (HSDE), Clarabel.rs handles quadratic objectives without requiring any epigraphical reformulation of the objective. It can therefore be significantly faster than other HSDE-based solvers for problems with quadratic objective functions. * __Infeasibility detection__: Infeasible problems are detected using a homogeneous embedding technique. * __Open Source__: Our code is available on [GitHub](https://github.com/oxfordcontrol/Clarabel.rs) and distributed under the Apache 2.0 License diff --git a/src/lib.rs b/src/lib.rs index 51d3cfcf..32c18150 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,7 @@ //! //! ## Features //! -//! * __Versatile__: Clarabel.rs solves linear programs (LPs), quadratic programs (QPs) and second-order cone programs (SOCPs). Future versions will provide support for problems involving positive semidefinite, exponential and power cones. +//! * __Versatile__: Clarabel.rs solves linear programs (LPs), quadratic programs (QPs), second-order cone programs (SOCPs) and semidefinite programs (SDPs). It also solves problems with exponential, power cone and generalized power cone constraints. //! //! * __Quadratic objectives__: Unlike interior point solvers based on the standard homogeneous self-dual embedding (HSDE), Clarabel.rs handles quadratic objectives without requiring any epigraphical reformulation of the objective. It can therefore be significantly faster than other HSDE-based solvers for problems with quadratic objective functions. //!