Skip to main content

runmat_runtime/builtins/math/optim/
coneprog.rs

1//! MATLAB-compatible `coneprog` and `secondordercone` builtins.
2
3use nalgebra::{DMatrix, DVector};
4use runmat_builtins::{
5    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
6    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
7    CellArray, StructValue, Tensor, Value,
8};
9use runmat_macros::runtime_builtin;
10
11use crate::builtins::common::spec::{
12    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
13    ReductionNaN, ResidencyPolicy, ShapeRequirements,
14};
15use crate::builtins::math::optim::common::{lookup_option, option_f64, option_usize};
16use crate::builtins::math::optim::type_resolvers::{linear_programming_type, optim_options_type};
17use crate::{build_runtime_error, BuiltinResult, RuntimeError};
18
19const CONEPROG: &str = "coneprog";
20const ALGORITHM: &str = "interior-point conic barrier";
21const DEFAULT_TOL: f64 = 1.0e-7;
22const DEFAULT_MAX_ITER: usize = 200;
23const STRICT_MARGIN: f64 = 1.0e-7;
24
25const OUTPUT_X: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
26    name: "x",
27    ty: BuiltinParamType::NumericArray,
28    arity: BuiltinParamArity::Required,
29    default: None,
30    description: "Optimal decision vector.",
31}];
32
33const OUTPUT_X_FVAL: [BuiltinParamDescriptor; 2] = [
34    BuiltinParamDescriptor {
35        name: "x",
36        ty: BuiltinParamType::NumericArray,
37        arity: BuiltinParamArity::Required,
38        default: None,
39        description: "Optimal decision vector.",
40    },
41    BuiltinParamDescriptor {
42        name: "fval",
43        ty: BuiltinParamType::NumericScalar,
44        arity: BuiltinParamArity::Required,
45        default: None,
46        description: "Objective value f'*x at the solution.",
47    },
48];
49
50const OUTPUT_X_FVAL_EXITFLAG: [BuiltinParamDescriptor; 3] = [
51    BuiltinParamDescriptor {
52        name: "x",
53        ty: BuiltinParamType::NumericArray,
54        arity: BuiltinParamArity::Required,
55        default: None,
56        description: "Optimal decision vector.",
57    },
58    BuiltinParamDescriptor {
59        name: "fval",
60        ty: BuiltinParamType::NumericScalar,
61        arity: BuiltinParamArity::Required,
62        default: None,
63        description: "Objective value f'*x at the solution.",
64    },
65    BuiltinParamDescriptor {
66        name: "exitflag",
67        ty: BuiltinParamType::NumericScalar,
68        arity: BuiltinParamArity::Required,
69        default: None,
70        description: "Solver status code.",
71    },
72];
73
74const OUTPUT_ALL: [BuiltinParamDescriptor; 5] = [
75    BuiltinParamDescriptor {
76        name: "x",
77        ty: BuiltinParamType::NumericArray,
78        arity: BuiltinParamArity::Required,
79        default: None,
80        description: "Optimal decision vector.",
81    },
82    BuiltinParamDescriptor {
83        name: "fval",
84        ty: BuiltinParamType::NumericScalar,
85        arity: BuiltinParamArity::Required,
86        default: None,
87        description: "Objective value f'*x at the solution.",
88    },
89    BuiltinParamDescriptor {
90        name: "exitflag",
91        ty: BuiltinParamType::NumericScalar,
92        arity: BuiltinParamArity::Required,
93        default: None,
94        description: "Solver status code.",
95    },
96    BuiltinParamDescriptor {
97        name: "output",
98        ty: BuiltinParamType::Any,
99        arity: BuiltinParamArity::Required,
100        default: None,
101        description: "Diagnostic metadata struct.",
102    },
103    BuiltinParamDescriptor {
104        name: "lambda",
105        ty: BuiltinParamType::Any,
106        arity: BuiltinParamArity::Required,
107        default: None,
108        description: "Lagrange multiplier compatibility struct.",
109    },
110];
111
112const INPUTS_CORE: [BuiltinParamDescriptor; 2] = [
113    BuiltinParamDescriptor {
114        name: "f",
115        ty: BuiltinParamType::NumericArray,
116        arity: BuiltinParamArity::Required,
117        default: None,
118        description: "Linear objective vector.",
119    },
120    BuiltinParamDescriptor {
121        name: "socConstraints",
122        ty: BuiltinParamType::Any,
123        arity: BuiltinParamArity::Required,
124        default: None,
125        description: "Second-order cone constraints from secondordercone.",
126    },
127];
128
129const INPUTS_LINEAR: [BuiltinParamDescriptor; 4] = [
130    BuiltinParamDescriptor {
131        name: "f",
132        ty: BuiltinParamType::NumericArray,
133        arity: BuiltinParamArity::Required,
134        default: None,
135        description: "Linear objective vector.",
136    },
137    BuiltinParamDescriptor {
138        name: "socConstraints",
139        ty: BuiltinParamType::Any,
140        arity: BuiltinParamArity::Required,
141        default: None,
142        description: "Second-order cone constraints from secondordercone.",
143    },
144    BuiltinParamDescriptor {
145        name: "A",
146        ty: BuiltinParamType::NumericArray,
147        arity: BuiltinParamArity::Optional,
148        default: Some("[]"),
149        description: "Inequality constraint matrix.",
150    },
151    BuiltinParamDescriptor {
152        name: "b",
153        ty: BuiltinParamType::NumericArray,
154        arity: BuiltinParamArity::Optional,
155        default: Some("[]"),
156        description: "Inequality constraint right-hand side.",
157    },
158];
159
160const INPUTS_FULL: [BuiltinParamDescriptor; 9] = [
161    BuiltinParamDescriptor {
162        name: "f",
163        ty: BuiltinParamType::NumericArray,
164        arity: BuiltinParamArity::Required,
165        default: None,
166        description: "Linear objective vector.",
167    },
168    BuiltinParamDescriptor {
169        name: "socConstraints",
170        ty: BuiltinParamType::Any,
171        arity: BuiltinParamArity::Required,
172        default: None,
173        description: "Second-order cone constraints from secondordercone.",
174    },
175    BuiltinParamDescriptor {
176        name: "A",
177        ty: BuiltinParamType::NumericArray,
178        arity: BuiltinParamArity::Optional,
179        default: Some("[]"),
180        description: "Inequality constraint matrix.",
181    },
182    BuiltinParamDescriptor {
183        name: "b",
184        ty: BuiltinParamType::NumericArray,
185        arity: BuiltinParamArity::Optional,
186        default: Some("[]"),
187        description: "Inequality constraint right-hand side.",
188    },
189    BuiltinParamDescriptor {
190        name: "Aeq",
191        ty: BuiltinParamType::NumericArray,
192        arity: BuiltinParamArity::Optional,
193        default: Some("[]"),
194        description: "Equality constraint matrix.",
195    },
196    BuiltinParamDescriptor {
197        name: "beq",
198        ty: BuiltinParamType::NumericArray,
199        arity: BuiltinParamArity::Optional,
200        default: Some("[]"),
201        description: "Equality constraint right-hand side.",
202    },
203    BuiltinParamDescriptor {
204        name: "lb",
205        ty: BuiltinParamType::NumericArray,
206        arity: BuiltinParamArity::Optional,
207        default: Some("[]"),
208        description: "Lower bounds.",
209    },
210    BuiltinParamDescriptor {
211        name: "ub",
212        ty: BuiltinParamType::NumericArray,
213        arity: BuiltinParamArity::Optional,
214        default: Some("[]"),
215        description: "Upper bounds.",
216    },
217    BuiltinParamDescriptor {
218        name: "options",
219        ty: BuiltinParamType::Any,
220        arity: BuiltinParamArity::Optional,
221        default: Some("optimoptions('coneprog')"),
222        description: "Options struct from optimoptions or optimset.",
223    },
224];
225
226const INPUTS_PROBLEM: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
227    name: "problem",
228    ty: BuiltinParamType::Any,
229    arity: BuiltinParamArity::Required,
230    default: None,
231    description: "Problem struct with f, socConstraints, linear constraints, bounds, and options.",
232}];
233
234const CONEPROG_SIGNATURES: [BuiltinSignatureDescriptor; 16] = [
235    BuiltinSignatureDescriptor {
236        label: "x = coneprog(f, socConstraints)",
237        inputs: &INPUTS_CORE,
238        outputs: &OUTPUT_X,
239    },
240    BuiltinSignatureDescriptor {
241        label: "x = coneprog(f, socConstraints, A, b)",
242        inputs: &INPUTS_LINEAR,
243        outputs: &OUTPUT_X,
244    },
245    BuiltinSignatureDescriptor {
246        label: "x = coneprog(f, socConstraints, A, b, Aeq, beq, lb, ub, options)",
247        inputs: &INPUTS_FULL,
248        outputs: &OUTPUT_X,
249    },
250    BuiltinSignatureDescriptor {
251        label: "x = coneprog(problem)",
252        inputs: &INPUTS_PROBLEM,
253        outputs: &OUTPUT_X,
254    },
255    BuiltinSignatureDescriptor {
256        label: "[x, fval] = coneprog(f, socConstraints)",
257        inputs: &INPUTS_CORE,
258        outputs: &OUTPUT_X_FVAL,
259    },
260    BuiltinSignatureDescriptor {
261        label: "[x, fval] = coneprog(f, socConstraints, A, b)",
262        inputs: &INPUTS_LINEAR,
263        outputs: &OUTPUT_X_FVAL,
264    },
265    BuiltinSignatureDescriptor {
266        label: "[x, fval] = coneprog(f, socConstraints, A, b, Aeq, beq, lb, ub, options)",
267        inputs: &INPUTS_FULL,
268        outputs: &OUTPUT_X_FVAL,
269    },
270    BuiltinSignatureDescriptor {
271        label: "[x, fval] = coneprog(problem)",
272        inputs: &INPUTS_PROBLEM,
273        outputs: &OUTPUT_X_FVAL,
274    },
275    BuiltinSignatureDescriptor {
276        label: "[x, fval, exitflag] = coneprog(f, socConstraints)",
277        inputs: &INPUTS_CORE,
278        outputs: &OUTPUT_X_FVAL_EXITFLAG,
279    },
280    BuiltinSignatureDescriptor {
281        label: "[x, fval, exitflag] = coneprog(f, socConstraints, A, b)",
282        inputs: &INPUTS_LINEAR,
283        outputs: &OUTPUT_X_FVAL_EXITFLAG,
284    },
285    BuiltinSignatureDescriptor {
286        label: "[x, fval, exitflag] = coneprog(f, socConstraints, A, b, Aeq, beq, lb, ub, options)",
287        inputs: &INPUTS_FULL,
288        outputs: &OUTPUT_X_FVAL_EXITFLAG,
289    },
290    BuiltinSignatureDescriptor {
291        label: "[x, fval, exitflag] = coneprog(problem)",
292        inputs: &INPUTS_PROBLEM,
293        outputs: &OUTPUT_X_FVAL_EXITFLAG,
294    },
295    BuiltinSignatureDescriptor {
296        label: "[x, fval, exitflag, output, lambda] = coneprog(f, socConstraints)",
297        inputs: &INPUTS_CORE,
298        outputs: &OUTPUT_ALL,
299    },
300    BuiltinSignatureDescriptor {
301        label: "[x, fval, exitflag, output, lambda] = coneprog(f, socConstraints, A, b)",
302        inputs: &INPUTS_LINEAR,
303        outputs: &OUTPUT_ALL,
304    },
305    BuiltinSignatureDescriptor {
306        label: "[x, fval, exitflag, output, lambda] = coneprog(f, socConstraints, A, b, Aeq, beq, lb, ub, options)",
307        inputs: &INPUTS_FULL,
308        outputs: &OUTPUT_ALL,
309    },
310    BuiltinSignatureDescriptor {
311        label: "[x, fval, exitflag, output, lambda] = coneprog(problem)",
312        inputs: &INPUTS_PROBLEM,
313        outputs: &OUTPUT_ALL,
314    },
315];
316
317const SOC_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
318    name: "socConstraint",
319    ty: BuiltinParamType::Any,
320    arity: BuiltinParamArity::Required,
321    default: None,
322    description: "Second-order cone constraint descriptor.",
323}];
324
325const SOC_INPUTS: [BuiltinParamDescriptor; 4] = [
326    BuiltinParamDescriptor {
327        name: "A",
328        ty: BuiltinParamType::NumericArray,
329        arity: BuiltinParamArity::Required,
330        default: None,
331        description: "Cone matrix in norm(A*x + b) <= d'*x + gamma.",
332    },
333    BuiltinParamDescriptor {
334        name: "b",
335        ty: BuiltinParamType::NumericArray,
336        arity: BuiltinParamArity::Required,
337        default: None,
338        description: "Cone offset vector.",
339    },
340    BuiltinParamDescriptor {
341        name: "d",
342        ty: BuiltinParamType::NumericArray,
343        arity: BuiltinParamArity::Required,
344        default: None,
345        description: "Linear cone right-hand-side vector.",
346    },
347    BuiltinParamDescriptor {
348        name: "gamma",
349        ty: BuiltinParamType::NumericScalar,
350        arity: BuiltinParamArity::Required,
351        default: None,
352        description: "Scalar cone right-hand-side offset.",
353    },
354];
355
356const SOC_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
357    label: "socConstraint = secondordercone(A, b, d, gamma)",
358    inputs: &SOC_INPUTS,
359    outputs: &SOC_OUTPUT,
360}];
361
362const ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
363    code: "RM.CONEPROG.INVALID_ARGUMENT",
364    identifier: Some("RunMat:coneprog:InvalidArgument"),
365    when: "The argument count or optional argument grammar is invalid.",
366    message: "coneprog: invalid argument",
367};
368
369const ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
370    code: "RM.CONEPROG.INVALID_INPUT",
371    identifier: Some("RunMat:coneprog:InvalidInput"),
372    when: "Objective, cone, constraint, option, or bound dimensions/types are invalid.",
373    message: "coneprog: invalid input",
374};
375
376const CONEPROG_ERRORS: [BuiltinErrorDescriptor; 2] = [ERROR_INVALID_ARGUMENT, ERROR_INVALID_INPUT];
377
378pub const CONEPROG_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
379    signatures: &CONEPROG_SIGNATURES,
380    output_mode: BuiltinOutputMode::ByRequestedOutputCount,
381    completion_policy: BuiltinCompletionPolicy::Public,
382    errors: &CONEPROG_ERRORS,
383};
384
385pub const SECONDORDERCONE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
386    signatures: &SOC_SIGNATURES,
387    output_mode: BuiltinOutputMode::Fixed,
388    completion_policy: BuiltinCompletionPolicy::Public,
389    errors: &CONEPROG_ERRORS,
390};
391
392#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::optim::coneprog")]
393pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
394    name: "coneprog",
395    op_kind: GpuOpKind::Custom("conic-programming"),
396    supported_precisions: &[],
397    broadcast: BroadcastSemantics::None,
398    provider_hooks: &[],
399    constant_strategy: ConstantStrategy::InlineLiteral,
400    residency: ResidencyPolicy::GatherImmediately,
401    nan_mode: ReductionNaN::Include,
402    two_pass_threshold: None,
403    workgroup_size: None,
404    accepts_nan_mode: false,
405    notes:
406        "Host conic interior-point solver. GPU-resident numeric inputs are gathered before solving.",
407};
408
409#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::optim::coneprog")]
410pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
411    name: "coneprog",
412    shape: ShapeRequirements::Any,
413    constant_strategy: ConstantStrategy::InlineLiteral,
414    elementwise: None,
415    reduction: None,
416    emits_nan: false,
417    notes: "Conic programming is a solver boundary and terminates fusion planning.",
418};
419
420#[runtime_builtin(
421    name = "secondordercone",
422    category = "math/optim",
423    summary = "Create a second-order cone constraint for coneprog.",
424    keywords = "secondordercone,coneprog,second order cone,socp,optimization",
425    accel = "cpu",
426    type_resolver(optim_options_type),
427    descriptor(crate::builtins::math::optim::coneprog::SECONDORDERCONE_DESCRIPTOR),
428    builtin_path = "crate::builtins::math::optim::coneprog"
429)]
430async fn secondordercone_builtin(
431    a: Value,
432    b: Value,
433    d: Value,
434    gamma: Value,
435) -> BuiltinResult<Value> {
436    let a = numeric_matrix("A", a).await?.ok_or_else(|| {
437        coneprog_error(&ERROR_INVALID_INPUT, "secondordercone: A must be nonempty")
438    })?;
439    let b = numeric_vector("b", b, FiniteMode::Finite).await?;
440    let d = numeric_vector("d", d, FiniteMode::Finite).await?;
441    let gamma = numeric_scalar("gamma", gamma, FiniteMode::Finite).await?;
442    validate_soc_dimensions(&a, &b, &d, gamma)?;
443
444    let mut st = StructValue::new();
445    st.insert("A", matrix_value(&a)?);
446    st.insert("b", vector_value_with_len(b));
447    st.insert("d", vector_value_with_len(d));
448    st.insert("gamma", Value::Num(gamma));
449    st.insert("Type", Value::from("secondordercone"));
450    Ok(Value::Struct(st))
451}
452
453#[runtime_builtin(
454    name = "coneprog",
455    category = "math/optim",
456    summary = "Solve a linear conic programming problem with second-order cone constraints.",
457    keywords = "coneprog,conic programming,second order cone,socp,optimization,bounds",
458    accel = "sink",
459    type_resolver(linear_programming_type),
460    descriptor(crate::builtins::math::optim::coneprog::CONEPROG_DESCRIPTOR),
461    builtin_path = "crate::builtins::math::optim::coneprog"
462)]
463async fn coneprog_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
464    let problem = parse_problem(args).await?;
465    Ok(finalize(solve_coneprog(&problem)))
466}
467
468#[derive(Clone, Copy)]
469enum FiniteMode {
470    Finite,
471    Bounds,
472}
473
474#[derive(Clone)]
475struct MatrixInput {
476    rows: usize,
477    cols: usize,
478    data: Vec<f64>,
479}
480
481impl MatrixInput {
482    fn row(&self, row: usize) -> Vec<f64> {
483        (0..self.cols)
484            .map(|col| self.data[row + col * self.rows])
485            .collect()
486    }
487
488    fn mul_vec(&self, x: &[f64]) -> Vec<f64> {
489        (0..self.rows).map(|row| dot(&self.row(row), x)).collect()
490    }
491}
492
493#[derive(Clone)]
494struct SocConstraint {
495    a: MatrixInput,
496    b: Vec<f64>,
497    d: Vec<f64>,
498    gamma: f64,
499}
500
501#[derive(Clone)]
502struct ConeProgram {
503    f: Vec<f64>,
504    soc: Vec<SocConstraint>,
505    a_ineq: Vec<Vec<f64>>,
506    b_ineq: Vec<f64>,
507    a_eq: Vec<Vec<f64>>,
508    b_eq: Vec<f64>,
509    lb: Vec<f64>,
510    ub: Vec<f64>,
511    options: SolverOptions,
512}
513
514#[derive(Clone, Copy)]
515struct SolverOptions {
516    tol: f64,
517    max_iter: usize,
518}
519
520#[derive(Clone)]
521struct ConeOutcome {
522    x: Option<Vec<f64>>,
523    fval: Option<f64>,
524    exitflag: i32,
525    iterations: usize,
526    constrviolation: f64,
527    message: String,
528    lambda: LambdaEstimate,
529}
530
531#[derive(Clone)]
532struct LambdaEstimate {
533    ineqlin: Vec<f64>,
534    eqlin: Vec<f64>,
535    lower: Vec<f64>,
536    upper: Vec<f64>,
537    soc: Vec<f64>,
538}
539
540impl ConeOutcome {
541    fn infeasible(problem: &ConeProgram, message: &str) -> Self {
542        Self {
543            x: None,
544            fval: None,
545            exitflag: -2,
546            iterations: 0,
547            constrviolation: 0.0,
548            message: message.to_string(),
549            lambda: LambdaEstimate::zeros(problem),
550        }
551    }
552
553    fn unbounded(problem: &ConeProgram) -> Self {
554        Self {
555            x: None,
556            fval: None,
557            exitflag: -3,
558            iterations: 0,
559            constrviolation: 0.0,
560            message: "Problem is unbounded.".to_string(),
561            lambda: LambdaEstimate::zeros(problem),
562        }
563    }
564}
565
566impl LambdaEstimate {
567    fn zeros(problem: &ConeProgram) -> Self {
568        Self {
569            ineqlin: vec![0.0; problem.a_ineq.len()],
570            eqlin: vec![0.0; problem.a_eq.len()],
571            lower: vec![0.0; problem.f.len()],
572            upper: vec![0.0; problem.f.len()],
573            soc: vec![0.0; problem.soc.len()],
574        }
575    }
576}
577
578async fn parse_problem(args: Vec<Value>) -> BuiltinResult<ConeProgram> {
579    if args.len() == 1 {
580        let value = gather(args.into_iter().next().unwrap()).await?;
581        let Value::Struct(problem) = value else {
582            return Err(coneprog_error(
583                &ERROR_INVALID_ARGUMENT,
584                "coneprog: single-argument form expects a problem struct",
585            ));
586        };
587        return parse_problem_struct(problem).await;
588    }
589    if args.len() < 2 {
590        return Err(coneprog_error(
591            &ERROR_INVALID_ARGUMENT,
592            "coneprog: expected f and socConstraints",
593        ));
594    }
595    let mut args = args.into_iter();
596    let f_value = args.next().unwrap();
597    let soc_value = args.next().unwrap();
598    let rest = args.collect::<Vec<_>>();
599
600    let f = numeric_vector("f", f_value, FiniteMode::Finite).await?;
601    if f.is_empty() {
602        return Err(coneprog_error(
603            &ERROR_INVALID_INPUT,
604            "coneprog: f must be a nonempty numeric vector",
605        ));
606    }
607    let n = f.len();
608    let soc = parse_soc_constraints(soc_value, n).await?;
609    let (rest, options) = split_options(rest).await?;
610    let (a_ineq, b_ineq, a_eq, b_eq, lb, ub) = parse_positional_constraints(rest, n).await?;
611    Ok(ConeProgram {
612        f,
613        soc,
614        a_ineq,
615        b_ineq,
616        a_eq,
617        b_eq,
618        lb,
619        ub,
620        options,
621    })
622}
623
624async fn parse_problem_struct(problem: StructValue) -> BuiltinResult<ConeProgram> {
625    let f = required_field(&problem, "f")?;
626    let f = numeric_vector("f", f, FiniteMode::Finite).await?;
627    if f.is_empty() {
628        return Err(coneprog_error(
629            &ERROR_INVALID_INPUT,
630            "coneprog: f must be a nonempty numeric vector",
631        ));
632    }
633    let n = f.len();
634    let soc_value = optional_field(&problem, "socConstraints")
635        .or_else(|| optional_field(&problem, "socConstraints".to_ascii_lowercase().as_str()))
636        .unwrap_or_else(empty_value);
637    let soc = parse_soc_constraints(soc_value, n).await?;
638    let a = optional_field(&problem, "Aineq")
639        .or_else(|| optional_field(&problem, "A"))
640        .unwrap_or_else(empty_value);
641    let b = optional_field(&problem, "bineq")
642        .or_else(|| optional_field(&problem, "b"))
643        .unwrap_or_else(empty_value);
644    let (a_ineq, b_ineq) = parse_constraint_pair("A", a, "b", b, n).await?;
645    let aeq = optional_field(&problem, "Aeq").unwrap_or_else(empty_value);
646    let beq = optional_field(&problem, "beq").unwrap_or_else(empty_value);
647    let (a_eq, b_eq) = parse_constraint_pair("Aeq", aeq, "beq", beq, n).await?;
648    let lb = optional_field(&problem, "lb").unwrap_or_else(empty_value);
649    let ub = optional_field(&problem, "ub").unwrap_or_else(empty_value);
650    let (lb, ub) = parse_bounds(Some(&lb), Some(&ub), n).await?;
651    let options = match optional_field(&problem, "options") {
652        Some(Value::Struct(st)) => parse_options(Some(&st))?,
653        Some(other) => {
654            return Err(coneprog_error(
655                &ERROR_INVALID_INPUT,
656                format!("coneprog: options must be a struct, got {other:?}"),
657            ))
658        }
659        None => SolverOptions::default(),
660    };
661    Ok(ConeProgram {
662        f,
663        soc,
664        a_ineq,
665        b_ineq,
666        a_eq,
667        b_eq,
668        lb,
669        ub,
670        options,
671    })
672}
673
674async fn split_options(rest: Vec<Value>) -> BuiltinResult<(Vec<Value>, SolverOptions)> {
675    let Some(last) = rest.last() else {
676        return Ok((rest, SolverOptions::default()));
677    };
678    let gathered = gather(last.clone()).await?;
679    if let Value::Struct(st) = gathered {
680        if is_options_struct(&st) {
681            let mut without = rest;
682            without.pop();
683            return Ok((without, parse_options(Some(&st))?));
684        }
685    }
686    Ok((rest, SolverOptions::default()))
687}
688
689async fn parse_positional_constraints(
690    rest: Vec<Value>,
691    n: usize,
692) -> BuiltinResult<(
693    Vec<Vec<f64>>,
694    Vec<f64>,
695    Vec<Vec<f64>>,
696    Vec<f64>,
697    Vec<f64>,
698    Vec<f64>,
699)> {
700    if !matches!(rest.len(), 0 | 2 | 4 | 6) {
701        return Err(coneprog_error(
702            &ERROR_INVALID_ARGUMENT,
703            "coneprog: optional linear constraints must be A,b,Aeq,beq,lb,ub",
704        ));
705    }
706    let a = rest.first().cloned().unwrap_or_else(empty_value);
707    let b = rest.get(1).cloned().unwrap_or_else(empty_value);
708    let (a_ineq, b_ineq) = parse_constraint_pair("A", a, "b", b, n).await?;
709    let aeq = rest.get(2).cloned().unwrap_or_else(empty_value);
710    let beq = rest.get(3).cloned().unwrap_or_else(empty_value);
711    let (a_eq, b_eq) = parse_constraint_pair("Aeq", aeq, "beq", beq, n).await?;
712    let (lb, ub) = parse_bounds(rest.get(4), rest.get(5), n).await?;
713    Ok((a_ineq, b_ineq, a_eq, b_eq, lb, ub))
714}
715
716fn is_options_struct(st: &StructValue) -> bool {
717    [
718        "Solver",
719        "TolX",
720        "TolFun",
721        "MaxIter",
722        "MaxFunEvals",
723        "Display",
724        "Algorithm",
725    ]
726    .iter()
727    .any(|name| lookup_option(st, name).is_some())
728}
729
730impl Default for SolverOptions {
731    fn default() -> Self {
732        Self {
733            tol: DEFAULT_TOL,
734            max_iter: DEFAULT_MAX_ITER,
735        }
736    }
737}
738
739fn parse_options(options: Option<&StructValue>) -> BuiltinResult<SolverOptions> {
740    let tol = option_f64(CONEPROG, options, "TolFun", DEFAULT_TOL)?.min(option_f64(
741        CONEPROG,
742        options,
743        "TolX",
744        DEFAULT_TOL,
745    )?);
746    let max_iter = option_usize(CONEPROG, options, "MaxIter", DEFAULT_MAX_ITER)?.max(1);
747    Ok(SolverOptions {
748        tol: tol.max(1.0e-12),
749        max_iter,
750    })
751}
752
753async fn parse_soc_constraints(value: Value, n: usize) -> BuiltinResult<Vec<SocConstraint>> {
754    let value = gather(value).await?;
755    if is_empty_value(&value) {
756        return Ok(Vec::new());
757    }
758    match value {
759        Value::Struct(st) => parse_single_soc(st, n).await.map(|constraint| vec![constraint]),
760        Value::Cell(cell) => {
761            let mut out = Vec::with_capacity(cell.data.len());
762            for value in cell.data {
763                let value = gather(value).await?;
764                let Value::Struct(st) = value else {
765                    return Err(coneprog_error(
766                        &ERROR_INVALID_INPUT,
767                        "coneprog: socConstraints cell entries must be secondordercone structs",
768                    ));
769                };
770                out.push(parse_single_soc(st, n).await?);
771            }
772            Ok(out)
773        }
774        Value::OutputList(values) => {
775            let mut out = Vec::with_capacity(values.len());
776            for value in values {
777                let value = gather(value).await?;
778                let Value::Struct(st) = value else {
779                    return Err(coneprog_error(
780                        &ERROR_INVALID_INPUT,
781                        "coneprog: socConstraints entries must be secondordercone structs",
782                    ));
783                };
784                out.push(parse_single_soc(st, n).await?);
785            }
786            Ok(out)
787        }
788        other => Err(coneprog_error(
789            &ERROR_INVALID_INPUT,
790            format!("coneprog: socConstraints must be a secondordercone struct or cell array, got {other:?}"),
791        )),
792    }
793}
794
795async fn parse_single_soc(st: StructValue, n: usize) -> BuiltinResult<SocConstraint> {
796    let a = numeric_matrix("socConstraints.A", required_field(&st, "A")?)
797        .await?
798        .ok_or_else(|| coneprog_error(&ERROR_INVALID_INPUT, "coneprog: cone A must be nonempty"))?;
799    let b = numeric_vector(
800        "socConstraints.b",
801        required_field(&st, "b")?,
802        FiniteMode::Finite,
803    )
804    .await?;
805    let d = numeric_vector(
806        "socConstraints.d",
807        required_field(&st, "d")?,
808        FiniteMode::Finite,
809    )
810    .await?;
811    let gamma = numeric_scalar(
812        "socConstraints.gamma",
813        required_field(&st, "gamma")?,
814        FiniteMode::Finite,
815    )
816    .await?;
817    validate_soc_dimensions(&a, &b, &d, gamma)?;
818    if a.cols != n || d.len() != n {
819        return Err(coneprog_error(
820            &ERROR_INVALID_INPUT,
821            "coneprog: cone A columns and d length must match f",
822        ));
823    }
824    Ok(SocConstraint { a, b, d, gamma })
825}
826
827fn validate_soc_dimensions(a: &MatrixInput, b: &[f64], d: &[f64], gamma: f64) -> BuiltinResult<()> {
828    if b.len() != a.rows {
829        return Err(coneprog_error(
830            &ERROR_INVALID_INPUT,
831            "secondordercone: b length must match rows of A",
832        ));
833    }
834    if d.len() != a.cols {
835        return Err(coneprog_error(
836            &ERROR_INVALID_INPUT,
837            "secondordercone: d length must match columns of A",
838        ));
839    }
840    if !gamma.is_finite() {
841        return Err(coneprog_error(
842            &ERROR_INVALID_INPUT,
843            "secondordercone: gamma must be finite",
844        ));
845    }
846    Ok(())
847}
848
849async fn gather(value: Value) -> BuiltinResult<Value> {
850    crate::dispatcher::gather_if_needed_async(&value)
851        .await
852        .map_err(|err| coneprog_error(&ERROR_INVALID_INPUT, err.message()))
853}
854
855fn required_field(st: &StructValue, name: &str) -> BuiltinResult<Value> {
856    optional_field(st, name).ok_or_else(|| {
857        coneprog_error(
858            &ERROR_INVALID_INPUT,
859            format!("coneprog: problem struct is missing {name}"),
860        )
861    })
862}
863
864fn optional_field(st: &StructValue, name: &str) -> Option<Value> {
865    st.fields
866        .iter()
867        .find(|(key, _)| key.eq_ignore_ascii_case(name))
868        .map(|(_, value)| value.clone())
869}
870
871fn is_empty_value(value: &Value) -> bool {
872    matches!(value, Value::Tensor(t) if t.data.is_empty())
873}
874
875fn empty_value() -> Value {
876    Value::Tensor(Tensor::zeros(vec![0, 0]))
877}
878
879async fn numeric_scalar(label: &str, value: Value, finite_mode: FiniteMode) -> BuiltinResult<f64> {
880    let values = numeric_vector(label, value, finite_mode).await?;
881    if values.len() == 1 {
882        Ok(values[0])
883    } else {
884        Err(coneprog_error(
885            &ERROR_INVALID_INPUT,
886            format!("coneprog: {label} must be a scalar"),
887        ))
888    }
889}
890
891async fn numeric_vector(
892    label: &str,
893    value: Value,
894    finite_mode: FiniteMode,
895) -> BuiltinResult<Vec<f64>> {
896    let value = gather(value).await?;
897    if is_empty_value(&value) {
898        return Ok(Vec::new());
899    }
900    let data = match value {
901        Value::Num(n) => vec![n],
902        Value::Int(i) => vec![i.to_f64()],
903        Value::Bool(flag) => vec![if flag { 1.0 } else { 0.0 }],
904        Value::Tensor(tensor) => {
905            if tensor.shape.len() > 2 || (tensor.rows() != 1 && tensor.cols() != 1) {
906                return Err(coneprog_error(
907                    &ERROR_INVALID_INPUT,
908                    format!("coneprog: {label} must be a vector"),
909                ));
910            }
911            tensor.data
912        }
913        other => {
914            return Err(coneprog_error(
915                &ERROR_INVALID_INPUT,
916                format!("coneprog: {label} must be a real numeric vector, got {other:?}"),
917            ))
918        }
919    };
920    validate_numbers(label, &data, finite_mode)?;
921    Ok(data)
922}
923
924async fn numeric_matrix(label: &str, value: Value) -> BuiltinResult<Option<MatrixInput>> {
925    let value = gather(value).await?;
926    if is_empty_value(&value) {
927        return Ok(None);
928    }
929    match value {
930        Value::Num(n) => {
931            validate_numbers(label, &[n], FiniteMode::Finite)?;
932            Ok(Some(MatrixInput {
933                rows: 1,
934                cols: 1,
935                data: vec![n],
936            }))
937        }
938        Value::Int(i) => {
939            let n = i.to_f64();
940            validate_numbers(label, &[n], FiniteMode::Finite)?;
941            Ok(Some(MatrixInput {
942                rows: 1,
943                cols: 1,
944                data: vec![n],
945            }))
946        }
947        Value::Tensor(tensor) => {
948            if tensor.shape.len() > 2 {
949                return Err(coneprog_error(
950                    &ERROR_INVALID_INPUT,
951                    format!("coneprog: {label} must be a numeric matrix"),
952                ));
953            }
954            validate_numbers(label, &tensor.data, FiniteMode::Finite)?;
955            Ok(Some(MatrixInput {
956                rows: tensor.rows(),
957                cols: tensor.cols(),
958                data: tensor.data,
959            }))
960        }
961        other => Err(coneprog_error(
962            &ERROR_INVALID_INPUT,
963            format!("coneprog: {label} must be a real numeric matrix, got {other:?}"),
964        )),
965    }
966}
967
968fn validate_numbers(label: &str, data: &[f64], finite_mode: FiniteMode) -> BuiltinResult<()> {
969    for value in data {
970        match finite_mode {
971            FiniteMode::Finite if !value.is_finite() => {
972                return Err(coneprog_error(
973                    &ERROR_INVALID_INPUT,
974                    format!("coneprog: {label} values must be finite"),
975                ))
976            }
977            FiniteMode::Bounds if value.is_nan() => {
978                return Err(coneprog_error(
979                    &ERROR_INVALID_INPUT,
980                    format!("coneprog: {label} bounds cannot be NaN"),
981                ))
982            }
983            _ => {}
984        }
985    }
986    Ok(())
987}
988
989async fn parse_constraint_pair(
990    matrix_label: &str,
991    matrix: Value,
992    rhs_label: &str,
993    rhs: Value,
994    n: usize,
995) -> BuiltinResult<(Vec<Vec<f64>>, Vec<f64>)> {
996    let matrix = numeric_matrix(matrix_label, matrix).await?;
997    let rhs = numeric_vector(rhs_label, rhs, FiniteMode::Finite).await?;
998    match (matrix, rhs.is_empty()) {
999        (None, true) => Ok((Vec::new(), Vec::new())),
1000        (None, false) => Err(coneprog_error(
1001            &ERROR_INVALID_INPUT,
1002            format!("coneprog: {matrix_label} cannot be empty when {rhs_label} is nonempty"),
1003        )),
1004        (Some(matrix), _) => {
1005            if matrix.cols != n {
1006                return Err(coneprog_error(
1007                    &ERROR_INVALID_INPUT,
1008                    format!("coneprog: {matrix_label} must have one column per element of f"),
1009                ));
1010            }
1011            if rhs.len() != matrix.rows {
1012                return Err(coneprog_error(
1013                    &ERROR_INVALID_INPUT,
1014                    format!("coneprog: {rhs_label} length must match rows of {matrix_label}"),
1015                ));
1016            }
1017            let rows = (0..matrix.rows).map(|row| matrix.row(row)).collect();
1018            Ok((rows, rhs))
1019        }
1020    }
1021}
1022
1023async fn parse_bounds(
1024    lb: Option<&Value>,
1025    ub: Option<&Value>,
1026    n: usize,
1027) -> BuiltinResult<(Vec<f64>, Vec<f64>)> {
1028    let lb = match lb {
1029        None => vec![f64::NEG_INFINITY; n],
1030        Some(value) if is_empty_value(value) => vec![f64::NEG_INFINITY; n],
1031        Some(value) => {
1032            let values = numeric_vector("lb", value.clone(), FiniteMode::Bounds).await?;
1033            normalize_bound("lb", values, n)?
1034        }
1035    };
1036    let ub = match ub {
1037        None => vec![f64::INFINITY; n],
1038        Some(value) if is_empty_value(value) => vec![f64::INFINITY; n],
1039        Some(value) => {
1040            let values = numeric_vector("ub", value.clone(), FiniteMode::Bounds).await?;
1041            normalize_bound("ub", values, n)?
1042        }
1043    };
1044    Ok((lb, ub))
1045}
1046
1047fn normalize_bound(label: &str, values: Vec<f64>, n: usize) -> BuiltinResult<Vec<f64>> {
1048    if values.len() == n {
1049        Ok(values)
1050    } else {
1051        Err(coneprog_error(
1052            &ERROR_INVALID_INPUT,
1053            format!("coneprog: {label} length must match f"),
1054        ))
1055    }
1056}
1057
1058fn solve_coneprog(problem: &ConeProgram) -> ConeOutcome {
1059    if let Some(reason) = impossible_bounds(problem) {
1060        return ConeOutcome::infeasible(problem, reason);
1061    }
1062    if problem.soc.is_empty()
1063        && problem.a_ineq.is_empty()
1064        && problem.a_eq.is_empty()
1065        && problem.lb.iter().all(|v| !v.is_finite())
1066        && problem.ub.iter().all(|v| !v.is_finite())
1067        && norm(&problem.f) > problem.options.tol
1068    {
1069        return ConeOutcome::unbounded(problem);
1070    }
1071
1072    let mut linear_rows = problem.a_ineq.clone();
1073    let mut linear_rhs = problem.b_ineq.clone();
1074    append_bound_rows(problem, &mut linear_rows, &mut linear_rhs);
1075
1076    let Some(mut x) = initial_strict_point(problem, &linear_rows, &linear_rhs) else {
1077        return ConeOutcome::infeasible(problem, "No feasible point found.");
1078    };
1079    if constraint_violation(problem, &x) > 1.0e-5 {
1080        return ConeOutcome::infeasible(problem, "No feasible point found.");
1081    }
1082    if !is_strictly_feasible(problem, &linear_rows, &linear_rhs, &x) {
1083        relax_into_interior(problem, &linear_rows, &linear_rhs, &mut x);
1084    }
1085    if !is_strictly_feasible(problem, &linear_rows, &linear_rhs, &x)
1086        && norm(&problem.f) <= problem.options.tol
1087    {
1088        let fval = dot(&problem.f, &x);
1089        return optimal_outcome(problem, x, fval, 0);
1090    }
1091    if !is_strictly_feasible(problem, &linear_rows, &linear_rhs, &x) {
1092        return ConeOutcome::infeasible(problem, "No strict feasible point found.");
1093    }
1094
1095    let mut iterations = 0usize;
1096    let outer = 9usize;
1097    let inner = (problem.options.max_iter / outer.max(1)).max(8);
1098    let mut t = 1.0;
1099    for _ in 0..outer {
1100        for _ in 0..inner {
1101            iterations += 1;
1102            let grad = barrier_gradient(problem, &linear_rows, &linear_rhs, &x, t);
1103            let projected = project_gradient(&problem.a_eq, &grad);
1104            let grad_norm = norm(&projected);
1105            if grad_norm <= problem.options.tol {
1106                break;
1107            }
1108            let current = barrier_value(problem, &linear_rows, &linear_rhs, &x, t);
1109            let mut step = 1.0 / (1.0 + grad_norm);
1110            let mut accepted = false;
1111            for _ in 0..40 {
1112                let mut candidate = x
1113                    .iter()
1114                    .zip(&projected)
1115                    .map(|(xi, gi)| xi - step * gi)
1116                    .collect::<Vec<_>>();
1117                project_to_equalities(&problem.a_eq, &problem.b_eq, &mut candidate);
1118                if is_strictly_feasible(problem, &linear_rows, &linear_rhs, &candidate)
1119                    && barrier_value(problem, &linear_rows, &linear_rhs, &candidate, t)
1120                        <= current - 1.0e-4 * step * grad_norm * grad_norm
1121                {
1122                    x = candidate;
1123                    accepted = true;
1124                    break;
1125                }
1126                step *= 0.5;
1127            }
1128            if !accepted {
1129                break;
1130            }
1131        }
1132        t *= 10.0;
1133    }
1134
1135    let violation = constraint_violation(problem, &x);
1136    if violation > 1.0e-4 {
1137        return ConeOutcome::infeasible(problem, "No feasible point found.");
1138    }
1139    let fval = dot(&problem.f, &x);
1140    optimal_outcome(problem, x, fval, iterations)
1141}
1142
1143fn impossible_bounds(problem: &ConeProgram) -> Option<&'static str> {
1144    for i in 0..problem.f.len() {
1145        if problem.lb[i] > problem.ub[i] + problem.options.tol {
1146            return Some("No feasible point found: lower bound exceeds upper bound.");
1147        }
1148    }
1149    None
1150}
1151
1152fn append_bound_rows(problem: &ConeProgram, rows: &mut Vec<Vec<f64>>, rhs: &mut Vec<f64>) {
1153    for i in 0..problem.f.len() {
1154        if problem.lb[i].is_finite() {
1155            let mut row = vec![0.0; problem.f.len()];
1156            row[i] = -1.0;
1157            rows.push(row);
1158            rhs.push(-problem.lb[i]);
1159        }
1160        if problem.ub[i].is_finite() {
1161            let mut row = vec![0.0; problem.f.len()];
1162            row[i] = 1.0;
1163            rows.push(row);
1164            rhs.push(problem.ub[i]);
1165        }
1166    }
1167}
1168
1169fn initial_strict_point(
1170    problem: &ConeProgram,
1171    linear_rows: &[Vec<f64>],
1172    linear_rhs: &[f64],
1173) -> Option<Vec<f64>> {
1174    let n = problem.f.len();
1175    let mut x = if problem.a_eq.is_empty() {
1176        vec![0.0; n]
1177    } else {
1178        pseudo_solve(&problem.a_eq, &problem.b_eq, n)?
1179    };
1180    for i in 0..n {
1181        x[i] = match (problem.lb[i].is_finite(), problem.ub[i].is_finite()) {
1182            (true, true) => 0.5 * (problem.lb[i] + problem.ub[i]),
1183            (true, false) if x[i] <= problem.lb[i] => problem.lb[i] + 1.0,
1184            (false, true) if x[i] >= problem.ub[i] => problem.ub[i] - 1.0,
1185            _ => x[i],
1186        };
1187    }
1188    project_to_equalities(&problem.a_eq, &problem.b_eq, &mut x);
1189    relax_into_interior(problem, linear_rows, linear_rhs, &mut x);
1190    Some(x)
1191}
1192
1193fn relax_into_interior(
1194    problem: &ConeProgram,
1195    linear_rows: &[Vec<f64>],
1196    linear_rhs: &[f64],
1197    x: &mut [f64],
1198) {
1199    for _ in 0..800 {
1200        let mut changed = false;
1201        for (row, rhs) in linear_rows.iter().zip(linear_rhs) {
1202            let slack = rhs - dot(row, x);
1203            if slack <= STRICT_MARGIN {
1204                let denom = dot(row, row).max(STRICT_MARGIN);
1205                let correction = (STRICT_MARGIN - slack) / denom;
1206                for (xi, ai) in x.iter_mut().zip(row) {
1207                    *xi -= correction * ai;
1208                }
1209                changed = true;
1210            }
1211        }
1212        for constraint in &problem.soc {
1213            let z = cone_z(constraint, x);
1214            let z_norm = norm(&z);
1215            let q = dot(&constraint.d, x) + constraint.gamma;
1216            let violation = z_norm - q + STRICT_MARGIN;
1217            if violation > 0.0 {
1218                let grad = cone_violation_gradient(constraint, &z, z_norm);
1219                let denom = dot(&grad, &grad).max(STRICT_MARGIN);
1220                for (xi, gi) in x.iter_mut().zip(&grad) {
1221                    *xi -= violation * gi / denom;
1222                }
1223                changed = true;
1224            }
1225        }
1226        project_to_equalities(&problem.a_eq, &problem.b_eq, x);
1227        if !changed {
1228            break;
1229        }
1230    }
1231}
1232
1233fn barrier_value(
1234    problem: &ConeProgram,
1235    linear_rows: &[Vec<f64>],
1236    linear_rhs: &[f64],
1237    x: &[f64],
1238    t: f64,
1239) -> f64 {
1240    let mut value = t * dot(&problem.f, x);
1241    for (row, rhs) in linear_rows.iter().zip(linear_rhs) {
1242        let slack = rhs - dot(row, x);
1243        if slack <= 0.0 {
1244            return f64::INFINITY;
1245        }
1246        value -= slack.ln();
1247    }
1248    for constraint in &problem.soc {
1249        let z = cone_z(constraint, x);
1250        let q = dot(&constraint.d, x) + constraint.gamma;
1251        let margin = q * q - dot(&z, &z);
1252        if q <= 0.0 || margin <= 0.0 {
1253            return f64::INFINITY;
1254        }
1255        value -= margin.ln();
1256    }
1257    value
1258}
1259
1260fn barrier_gradient(
1261    problem: &ConeProgram,
1262    linear_rows: &[Vec<f64>],
1263    linear_rhs: &[f64],
1264    x: &[f64],
1265    t: f64,
1266) -> Vec<f64> {
1267    let mut grad = problem.f.iter().map(|value| t * value).collect::<Vec<_>>();
1268    for (row, rhs) in linear_rows.iter().zip(linear_rhs) {
1269        let slack = (rhs - dot(row, x)).max(STRICT_MARGIN);
1270        for (gi, ai) in grad.iter_mut().zip(row) {
1271            *gi += ai / slack;
1272        }
1273    }
1274    for constraint in &problem.soc {
1275        let z = cone_z(constraint, x);
1276        let q = dot(&constraint.d, x) + constraint.gamma;
1277        let margin = (q * q - dot(&z, &z)).max(STRICT_MARGIN);
1278        let mut margin_grad = constraint
1279            .d
1280            .iter()
1281            .map(|di| 2.0 * q * di)
1282            .collect::<Vec<_>>();
1283        for col in 0..constraint.a.cols {
1284            let a_col_z = (0..constraint.a.rows)
1285                .map(|row| constraint.a.data[row + col * constraint.a.rows] * z[row])
1286                .sum::<f64>();
1287            margin_grad[col] -= 2.0 * a_col_z;
1288        }
1289        for (gi, mi) in grad.iter_mut().zip(margin_grad) {
1290            *gi -= mi / margin;
1291        }
1292    }
1293    grad
1294}
1295
1296fn project_gradient(a_eq: &[Vec<f64>], grad: &[f64]) -> Vec<f64> {
1297    if a_eq.is_empty() {
1298        return grad.to_vec();
1299    }
1300    let mut projected = grad.to_vec();
1301    let rhs = a_eq.iter().map(|row| dot(row, grad)).collect::<Vec<_>>();
1302    if let Some(rowspace) = minimum_norm_equality_correction(a_eq, &rhs, grad.len()) {
1303        for (pi, ri) in projected.iter_mut().zip(rowspace) {
1304            *pi -= ri;
1305        }
1306    }
1307    projected
1308}
1309
1310fn project_to_equalities(a_eq: &[Vec<f64>], b_eq: &[f64], x: &mut [f64]) {
1311    if a_eq.is_empty() {
1312        return;
1313    }
1314    let residual = a_eq
1315        .iter()
1316        .zip(b_eq)
1317        .map(|(row, rhs)| rhs - dot(row, x))
1318        .collect::<Vec<_>>();
1319    if let Some(correction) = minimum_norm_equality_correction(a_eq, &residual, x.len()) {
1320        for (xi, ci) in x.iter_mut().zip(correction) {
1321            *xi += ci;
1322        }
1323    }
1324}
1325
1326fn minimum_norm_equality_correction(rows: &[Vec<f64>], rhs: &[f64], n: usize) -> Option<Vec<f64>> {
1327    if rows.is_empty() {
1328        return Some(vec![0.0; n]);
1329    }
1330    let a = dmatrix_from_rows(rows, n);
1331    let rhs = DVector::from_column_slice(rhs);
1332    let gram = &a * a.transpose();
1333    let svd = gram.svd(true, true);
1334    let y = svd.solve(&rhs, DEFAULT_TOL).ok()?;
1335    let correction = a.transpose() * y;
1336    Some(correction.iter().copied().collect())
1337}
1338
1339fn pseudo_solve(rows: &[Vec<f64>], rhs: &[f64], n: usize) -> Option<Vec<f64>> {
1340    if rows.is_empty() {
1341        return Some(vec![0.0; n]);
1342    }
1343    let matrix = dmatrix_from_rows(rows, n);
1344    let rhs_vec = DVector::from_column_slice(rhs);
1345    let svd = matrix.svd(true, true);
1346    let x = svd.solve(&rhs_vec, DEFAULT_TOL).ok()?;
1347    let out = x.iter().copied().collect::<Vec<_>>();
1348    let residual = rows
1349        .iter()
1350        .zip(rhs)
1351        .map(|(row, target)| (dot(row, &out) - target).abs())
1352        .fold(0.0, f64::max);
1353    (residual <= 1.0e-7).then_some(out)
1354}
1355
1356fn dmatrix_from_rows(rows: &[Vec<f64>], n: usize) -> DMatrix<f64> {
1357    let data = rows
1358        .iter()
1359        .flat_map(|row| row.iter().copied())
1360        .collect::<Vec<_>>();
1361    DMatrix::from_row_slice(rows.len(), n, &data)
1362}
1363
1364fn is_strictly_feasible(
1365    problem: &ConeProgram,
1366    linear_rows: &[Vec<f64>],
1367    linear_rhs: &[f64],
1368    x: &[f64],
1369) -> bool {
1370    linear_rows
1371        .iter()
1372        .zip(linear_rhs)
1373        .all(|(row, rhs)| rhs - dot(row, x) > STRICT_MARGIN)
1374        && problem.soc.iter().all(|constraint| {
1375            let z = cone_z(constraint, x);
1376            let q = dot(&constraint.d, x) + constraint.gamma;
1377            q > 0.0 && q - norm(&z) > STRICT_MARGIN
1378        })
1379        && equality_violation(problem, x) <= 1.0e-6
1380}
1381
1382fn constraint_violation(problem: &ConeProgram, x: &[f64]) -> f64 {
1383    let mut violation = equality_violation(problem, x);
1384    for (row, rhs) in problem.a_ineq.iter().zip(&problem.b_ineq) {
1385        violation = violation.max((dot(row, x) - rhs).max(0.0));
1386    }
1387    for i in 0..x.len() {
1388        violation = violation.max((problem.lb[i] - x[i]).max(0.0));
1389        violation = violation.max((x[i] - problem.ub[i]).max(0.0));
1390    }
1391    for constraint in &problem.soc {
1392        let z = cone_z(constraint, x);
1393        let q = dot(&constraint.d, x) + constraint.gamma;
1394        violation = violation.max((norm(&z) - q).max(0.0));
1395    }
1396    violation
1397}
1398
1399fn equality_violation(problem: &ConeProgram, x: &[f64]) -> f64 {
1400    problem
1401        .a_eq
1402        .iter()
1403        .zip(&problem.b_eq)
1404        .map(|(row, rhs)| (dot(row, x) - rhs).abs())
1405        .fold(0.0, f64::max)
1406}
1407
1408fn cone_z(constraint: &SocConstraint, x: &[f64]) -> Vec<f64> {
1409    let mut z = constraint.a.mul_vec(x);
1410    for (zi, bi) in z.iter_mut().zip(&constraint.b) {
1411        *zi += bi;
1412    }
1413    z
1414}
1415
1416fn cone_violation_gradient(constraint: &SocConstraint, z: &[f64], z_norm: f64) -> Vec<f64> {
1417    let mut grad = constraint.d.iter().map(|di| -*di).collect::<Vec<_>>();
1418    if z_norm > STRICT_MARGIN {
1419        for col in 0..constraint.a.cols {
1420            let a_col_z = (0..constraint.a.rows)
1421                .map(|row| constraint.a.data[row + col * constraint.a.rows] * z[row])
1422                .sum::<f64>();
1423            grad[col] += a_col_z / z_norm;
1424        }
1425    }
1426    grad
1427}
1428
1429fn optimal_outcome(
1430    problem: &ConeProgram,
1431    x: Vec<f64>,
1432    fval: f64,
1433    iterations: usize,
1434) -> ConeOutcome {
1435    let lambda = estimate_lambda(problem, &x);
1436    ConeOutcome {
1437        x: Some(x.clone()),
1438        fval: Some(fval),
1439        exitflag: 1,
1440        iterations,
1441        constrviolation: constraint_violation(problem, &x),
1442        message: "Optimal solution found.".to_string(),
1443        lambda,
1444    }
1445}
1446
1447#[derive(Clone, Copy)]
1448enum ActiveConstraint {
1449    Ineq(usize),
1450    Eq(usize),
1451    Lower(usize),
1452    Upper(usize),
1453    Soc(usize),
1454}
1455
1456fn estimate_lambda(problem: &ConeProgram, x: &[f64]) -> LambdaEstimate {
1457    let mut lambda = LambdaEstimate::zeros(problem);
1458    let mut columns = Vec::new();
1459    let mut kinds = Vec::new();
1460    let active_tol = problem.options.tol.max(1.0e-6);
1461
1462    for (idx, (row, rhs)) in problem.a_ineq.iter().zip(&problem.b_ineq).enumerate() {
1463        if rhs - dot(row, x) <= active_tol {
1464            columns.push(DVector::from_column_slice(row));
1465            kinds.push(ActiveConstraint::Ineq(idx));
1466        }
1467    }
1468    for (idx, row) in problem.a_eq.iter().enumerate() {
1469        columns.push(DVector::from_column_slice(row));
1470        kinds.push(ActiveConstraint::Eq(idx));
1471    }
1472    for idx in 0..problem.f.len() {
1473        if problem.lb[idx].is_finite() && x[idx] - problem.lb[idx] <= active_tol {
1474            let mut column = vec![0.0; problem.f.len()];
1475            column[idx] = -1.0;
1476            columns.push(DVector::from_column_slice(&column));
1477            kinds.push(ActiveConstraint::Lower(idx));
1478        }
1479        if problem.ub[idx].is_finite() && problem.ub[idx] - x[idx] <= active_tol {
1480            let mut column = vec![0.0; problem.f.len()];
1481            column[idx] = 1.0;
1482            columns.push(DVector::from_column_slice(&column));
1483            kinds.push(ActiveConstraint::Upper(idx));
1484        }
1485    }
1486    for (idx, constraint) in problem.soc.iter().enumerate() {
1487        let z = cone_z(constraint, x);
1488        let z_norm = norm(&z);
1489        let q = dot(&constraint.d, x) + constraint.gamma;
1490        if q - z_norm <= active_tol {
1491            let column = cone_violation_gradient(constraint, &z, z_norm);
1492            if norm(&column) > DEFAULT_TOL {
1493                columns.push(DVector::from_column_slice(&column));
1494                kinds.push(ActiveConstraint::Soc(idx));
1495            }
1496        }
1497    }
1498
1499    if columns.is_empty() {
1500        return lambda;
1501    }
1502
1503    let matrix = DMatrix::from_columns(&columns);
1504    let rhs = DVector::from_iterator(problem.f.len(), problem.f.iter().map(|value| -*value));
1505    let Some(solution) = matrix.svd(true, true).solve(&rhs, DEFAULT_TOL).ok() else {
1506        return lambda;
1507    };
1508    for (value, kind) in solution.iter().zip(kinds) {
1509        match kind {
1510            ActiveConstraint::Ineq(idx) => lambda.ineqlin[idx] = (*value).max(0.0),
1511            ActiveConstraint::Eq(idx) => lambda.eqlin[idx] = *value,
1512            ActiveConstraint::Lower(idx) => lambda.lower[idx] = (*value).max(0.0),
1513            ActiveConstraint::Upper(idx) => lambda.upper[idx] = (*value).max(0.0),
1514            ActiveConstraint::Soc(idx) => lambda.soc[idx] = (*value).max(0.0),
1515        }
1516    }
1517    lambda
1518}
1519
1520fn finalize(outcome: ConeOutcome) -> Value {
1521    let x = outcome
1522        .x
1523        .clone()
1524        .map(vector_value_with_len)
1525        .unwrap_or_else(empty_value);
1526    let fval = outcome.fval.map(Value::Num).unwrap_or_else(empty_value);
1527    let exitflag = Value::Num(outcome.exitflag as f64);
1528    let output = Value::Struct(build_output_struct(&outcome));
1529    let lambda = Value::Struct(build_lambda_struct(&outcome));
1530
1531    match crate::output_count::current_output_count() {
1532        None => x,
1533        Some(0) => Value::OutputList(Vec::new()),
1534        Some(1) => crate::output_count::output_list_with_padding(1, vec![x]),
1535        Some(2) => crate::output_count::output_list_with_padding(2, vec![x, fval]),
1536        Some(3) => crate::output_count::output_list_with_padding(3, vec![x, fval, exitflag]),
1537        Some(4) => {
1538            crate::output_count::output_list_with_padding(4, vec![x, fval, exitflag, output])
1539        }
1540        Some(n) => crate::output_count::output_list_with_padding(
1541            n,
1542            vec![x, fval, exitflag, output, lambda],
1543        ),
1544    }
1545}
1546
1547fn build_output_struct(outcome: &ConeOutcome) -> StructValue {
1548    let mut fields = StructValue::new();
1549    fields.insert("iterations", Value::Num(outcome.iterations as f64));
1550    fields.insert("algorithm", Value::from(ALGORITHM));
1551    fields.insert("constrviolation", Value::Num(outcome.constrviolation));
1552    fields.insert("message", Value::from(outcome.message.clone()));
1553    fields
1554}
1555
1556fn build_lambda_struct(outcome: &ConeOutcome) -> StructValue {
1557    let mut fields = StructValue::new();
1558    fields.insert(
1559        "ineqlin",
1560        vector_value_with_len(outcome.lambda.ineqlin.clone()),
1561    );
1562    fields.insert("eqlin", vector_value_with_len(outcome.lambda.eqlin.clone()));
1563    fields.insert("lower", vector_value_with_len(outcome.lambda.lower.clone()));
1564    fields.insert("upper", vector_value_with_len(outcome.lambda.upper.clone()));
1565    let soc = outcome
1566        .lambda
1567        .soc
1568        .iter()
1569        .map(|value| Value::Num(*value))
1570        .collect::<Vec<_>>();
1571    fields.insert(
1572        "soc",
1573        Value::Cell(
1574            CellArray::new(soc, outcome.lambda.soc.len().max(1), 1).unwrap_or_else(|_| {
1575                CellArray::new(Vec::new(), 0, 0).expect("empty lambda soc cell")
1576            }),
1577        ),
1578    );
1579    fields
1580}
1581
1582fn vector_value_with_len(values: Vec<f64>) -> Value {
1583    let n = values.len();
1584    Tensor::new(values, vec![n, 1])
1585        .map(Value::Tensor)
1586        .unwrap_or_else(|_| empty_value())
1587}
1588
1589fn matrix_value(matrix: &MatrixInput) -> BuiltinResult<Value> {
1590    Tensor::new(matrix.data.clone(), vec![matrix.rows, matrix.cols])
1591        .map(Value::Tensor)
1592        .map_err(|err| coneprog_error(&ERROR_INVALID_INPUT, err))
1593}
1594
1595fn dot(a: &[f64], b: &[f64]) -> f64 {
1596    a.iter().zip(b).map(|(x, y)| x * y).sum()
1597}
1598
1599fn norm(values: &[f64]) -> f64 {
1600    dot(values, values).sqrt()
1601}
1602
1603fn coneprog_error(error: &'static BuiltinErrorDescriptor, detail: impl AsRef<str>) -> RuntimeError {
1604    let detail = detail.as_ref();
1605    let message = if detail.starts_with("coneprog:") || detail.starts_with("secondordercone:") {
1606        detail.to_string()
1607    } else {
1608        format!("{}: {detail}", error.message)
1609    };
1610    let mut builder = build_runtime_error(message).with_builtin(CONEPROG);
1611    if let Some(identifier) = error.identifier {
1612        builder = builder.with_identifier(identifier);
1613    }
1614    builder.build()
1615}
1616
1617#[cfg(test)]
1618mod tests {
1619    use super::*;
1620    use futures::executor::block_on;
1621
1622    fn tensor(data: Vec<f64>, rows: usize, cols: usize) -> Value {
1623        Value::Tensor(Tensor::new(data, vec![rows, cols]).unwrap())
1624    }
1625
1626    fn empty() -> Value {
1627        Value::Tensor(Tensor::zeros(vec![0, 0]))
1628    }
1629
1630    fn run(args: Vec<Value>, outputs: usize) -> Vec<Value> {
1631        let _guard = crate::output_count::push_output_count(Some(outputs));
1632        let value = block_on(coneprog_builtin(args)).expect("coneprog");
1633        match value {
1634            Value::OutputList(values) => values,
1635            other => vec![other],
1636        }
1637    }
1638
1639    fn soc_unit_disk() -> Value {
1640        block_on(secondordercone_builtin(
1641            tensor(vec![1.0, 0.0, 0.0, 1.0], 2, 2),
1642            tensor(vec![0.0, 0.0], 2, 1),
1643            tensor(vec![0.0, 0.0], 2, 1),
1644            Value::Num(1.0),
1645        ))
1646        .unwrap()
1647    }
1648
1649    #[test]
1650    fn secondordercone_constructs_descriptor() {
1651        let Value::Struct(st) = soc_unit_disk() else {
1652            panic!("expected struct");
1653        };
1654        assert!(st.fields.contains_key("A"));
1655        assert!(st.fields.contains_key("b"));
1656        assert!(st.fields.contains_key("d"));
1657        assert_eq!(st.fields.get("Type"), Some(&Value::from("secondordercone")));
1658    }
1659
1660    #[test]
1661    fn coneprog_solves_soc_with_linear_bound() {
1662        let outputs = run(
1663            vec![
1664                tensor(vec![-1.0, 0.0], 2, 1),
1665                soc_unit_disk(),
1666                tensor(vec![1.0, 0.0], 1, 2),
1667                Value::Num(0.75),
1668                empty(),
1669                empty(),
1670                empty(),
1671                empty(),
1672            ],
1673            5,
1674        );
1675        let Value::Tensor(x) = &outputs[0] else {
1676            panic!("expected x");
1677        };
1678        assert!((x.data[0] - 0.75).abs() < 1.0e-3, "{x:?}");
1679        assert!(x.data[1].abs() < 1.0e-3, "{x:?}");
1680        assert!(matches!(&outputs[1], Value::Num(fval) if (*fval + 0.75).abs() < 1.0e-3));
1681        assert!(matches!(&outputs[2], Value::Num(flag) if *flag == 1.0));
1682        assert!(matches!(&outputs[3], Value::Struct(st) if st.fields.contains_key("algorithm")));
1683        let Value::Struct(lambda) = &outputs[4] else {
1684            panic!("expected lambda struct");
1685        };
1686        let Value::Tensor(ineqlin) = lambda.fields.get("ineqlin").unwrap() else {
1687            panic!("expected ineqlin multipliers");
1688        };
1689        assert!((ineqlin.data[0] - 1.0).abs() < 1.0e-2, "{ineqlin:?}");
1690        assert!(lambda.fields.contains_key("soc"));
1691    }
1692
1693    #[test]
1694    fn coneprog_accepts_problem_struct_and_options() {
1695        let mut options = StructValue::new();
1696        options.insert("Solver", Value::from("coneprog"));
1697        options.insert("TolFun", Value::Num(1.0e-8));
1698        options.insert("MaxIter", Value::Num(120.0));
1699
1700        let mut problem = StructValue::new();
1701        problem.insert("f", tensor(vec![0.0, 1.0], 2, 1));
1702        problem.insert("socConstraints", soc_unit_disk());
1703        problem.insert("Aineq", tensor(vec![0.0, -1.0], 1, 2));
1704        problem.insert("bineq", Value::Num(0.25));
1705        problem.insert("options", Value::Struct(options));
1706
1707        let outputs = run(vec![Value::Struct(problem)], 3);
1708        let Value::Tensor(x) = &outputs[0] else {
1709            panic!("expected x");
1710        };
1711        assert!((x.data[1] + 0.25).abs() < 1.0e-3, "{x:?}");
1712        assert!(matches!(&outputs[2], Value::Num(flag) if *flag == 1.0));
1713    }
1714
1715    #[test]
1716    fn coneprog_reports_infeasible_bounds() {
1717        let outputs = run(
1718            vec![
1719                tensor(vec![1.0, 0.0], 2, 1),
1720                empty(),
1721                empty(),
1722                empty(),
1723                empty(),
1724                empty(),
1725                tensor(vec![2.0, 0.0], 2, 1),
1726                tensor(vec![1.0, 1.0], 2, 1),
1727            ],
1728            4,
1729        );
1730        assert!(matches!(&outputs[0], Value::Tensor(t) if t.data.is_empty()));
1731        assert!(matches!(&outputs[2], Value::Num(flag) if *flag == -2.0));
1732    }
1733
1734    #[test]
1735    fn coneprog_validates_cone_dimensions() {
1736        let err = block_on(secondordercone_builtin(
1737            tensor(vec![1.0, 0.0], 1, 2),
1738            tensor(vec![0.0, 0.0], 2, 1),
1739            tensor(vec![0.0, 0.0], 2, 1),
1740            Value::Num(1.0),
1741        ))
1742        .unwrap_err();
1743        assert_eq!(err.identifier(), Some("RunMat:coneprog:InvalidInput"));
1744    }
1745}